UNPKG

@dcodegroup/vue-mention

Version:

Mention popper for input and textarea

493 lines (492 loc) 15.4 kB
import { options, Dropdown } from "floating-vue"; import { defineComponent, ref, watch, computed, onMounted, onUpdated, onUnmounted, nextTick, resolveComponent, openBlock, createElementBlock, normalizeClass, renderSlot, createVNode, mergeProps, withCtx, createTextVNode, Fragment, renderList, toDisplayString, createElementVNode, normalizeStyle } from "vue"; var textareaCaret = { exports: {} }; (function(module) { (function() { var properties = [ "direction", "boxSizing", "width", "height", "overflowX", "overflowY", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderStyle", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "fontStyle", "fontVariant", "fontWeight", "fontStretch", "fontSize", "fontSizeAdjust", "lineHeight", "fontFamily", "textAlign", "textTransform", "textIndent", "textDecoration", "letterSpacing", "wordSpacing", "tabSize", "MozTabSize" ]; var isBrowser = typeof window !== "undefined"; var isFirefox = isBrowser && window.mozInnerScreenX != null; function getCaretCoordinates(element, position, options2) { if (!isBrowser) { throw new Error("textarea-caret-position#getCaretCoordinates should only be called in a browser"); } var debug = options2 && options2.debug || false; if (debug) { var el = document.querySelector("#input-textarea-caret-position-mirror-div"); if (el) el.parentNode.removeChild(el); } var div = document.createElement("div"); div.id = "input-textarea-caret-position-mirror-div"; document.body.appendChild(div); var style = div.style; var computed2 = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; var isInput = element.nodeName === "INPUT"; style.whiteSpace = "pre-wrap"; if (!isInput) style.wordWrap = "break-word"; style.position = "absolute"; if (!debug) style.visibility = "hidden"; properties.forEach(function(prop) { if (isInput && prop === "lineHeight") { style.lineHeight = computed2.height; } else { style[prop] = computed2[prop]; } }); if (isFirefox) { if (element.scrollHeight > parseInt(computed2.height)) style.overflowY = "scroll"; } else { style.overflow = "hidden"; } div.textContent = element.value.substring(0, position); if (isInput) div.textContent = div.textContent.replace(/\s/g, "\xA0"); var span = document.createElement("span"); span.textContent = element.value.substring(position) || "."; div.appendChild(span); var coordinates = { top: span.offsetTop + parseInt(computed2["borderTopWidth"]), left: span.offsetLeft + parseInt(computed2["borderLeftWidth"]), height: parseInt(computed2["lineHeight"]) }; if (debug) { span.style.backgroundColor = "#aaa"; } else { document.body.removeChild(div); } return coordinates; } { module.exports = getCaretCoordinates; } })(); })(textareaCaret); var getCaretPosition = textareaCaret.exports; var _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; options.themes.mentionable = { $extend: "dropdown", placement: "top-start", arrowPadding: 6, arrowOverflow: false }; const _sfc_main = defineComponent({ components: { VDropdown: Dropdown }, inheritAttrs: false, props: { keys: { type: Array, required: true }, items: { type: Array, default: () => [] }, omitKey: { type: Boolean, default: false }, filteringDisabled: { type: Boolean, default: false }, insertSpace: { type: Boolean, default: false }, allowSpace: { type: Boolean, default: false }, mapInsert: { type: Function, default: null }, limit: { type: Number, default: 8 }, theme: { type: String, default: "mentionable" }, caretHeight: { type: Number, default: 0 } }, emits: ["search", "open", "close", "apply"], setup(props, { emit }) { const currentKey = ref(null); let currentKeyIndex; const oldKey = ref(null); const isMentioning = ref(false); const searchText = ref(null); watch(searchText, (value, oldValue) => { if (value) { emit("search", value, oldValue); } }); const filteredItems = computed(() => { if (!searchText.value || props.filteringDisabled) { return props.items; } const finalSearchText = searchText.value.toLowerCase(); return props.items.filter((item) => { let text; if (item.searchText) { text = item.searchText; } else if (item.label) { text = item.label; } else { text = ""; for (const key in item) { text += item[key]; } } return text.toLowerCase().includes(finalSearchText); }); }); const displayedItems = computed(() => filteredItems.value.slice(0, props.limit)); const selectedIndex = ref(0); watch(displayedItems, () => { selectedIndex.value = 0; }, { deep: true }); let input; const el = ref(null); function getInput() { var _a, _b; return (_b = (_a = el.value.querySelector("input")) != null ? _a : el.value.querySelector("textarea")) != null ? _b : el.value.querySelector('[contenteditable="true"]'); } onMounted(() => { input = getInput(); attach(); }); onUpdated(() => { const newInput = getInput(); if (newInput !== input) { detach(); input = newInput; attach(); } }); onUnmounted(() => { detach(); }); function attach() { if (input) { input.addEventListener("input", onInput); input.addEventListener("keydown", onKeyDown); input.addEventListener("keyup", onKeyUp); input.addEventListener("scroll", onScroll); input.addEventListener("blur", onBlur); } } function detach() { if (input) { input.removeEventListener("input", onInput); input.removeEventListener("keydown", onKeyDown); input.removeEventListener("keyup", onKeyUp); input.removeEventListener("scroll", onScroll); input.removeEventListener("blur", onBlur); } } function onInput() { checkKey(); } function onBlur() { closeMenu(); } function onKeyDown(e) { if (currentKey.value) { if (e.key === "ArrowDown") { selectedIndex.value++; if (selectedIndex.value >= displayedItems.value.length) { selectedIndex.value = 0; } cancelEvent(e); } if (e.key === "ArrowUp") { selectedIndex.value--; if (selectedIndex.value < 0) { selectedIndex.value = displayedItems.value.length - 1; } cancelEvent(e); } if ((e.key === "Enter" || e.key === "Tab") && displayedItems.value.length > 0) { applyMention(selectedIndex.value); cancelEvent(e); } if (e.key === "Escape") { closeMenu(); cancelEvent(e); } } } let cancelKeyUp = null; function onKeyUp(e) { if (cancelKeyUp && e.key === cancelKeyUp) { cancelEvent(e); } cancelKeyUp = null; } function cancelEvent(e) { e.preventDefault(); e.stopPropagation(); cancelKeyUp = e.key; } function onScroll() { updateCaretPosition(); } function getSelectionStart() { return input.isContentEditable ? window.getSelection().anchorOffset : input.selectionStart; } function setCaretPosition(index) { nextTick(() => { input.selectionEnd = index; }); } function getValue() { return input.isContentEditable ? window.getSelection().anchorNode.textContent : input.value; } function setValue(value) { input.value = value; emitInputEvent("input"); } function emitInputEvent(type) { input.dispatchEvent(new Event(type)); } let lastSearchText = null; function checkKey() { const index = getSelectionStart(); if (index >= 0) { const { key, keyIndex } = getLastKeyBeforeCaret(index); const text = lastSearchText = getLastSearchText(index, keyIndex); if (!(keyIndex < 1 || /\s/.test(getValue()[keyIndex - 1]))) { return false; } const keyIsBeforeCaret = getValue()[index - 1] === key; const shouldOpen = props.allowSpace ? isMentioning.value || keyIsBeforeCaret : true; if (text != null && shouldOpen) { openMenu(key, keyIndex); searchText.value = text; return true; } } closeMenu(); return false; } function getLastKeyBeforeCaret(caretIndex) { const [keyData] = props.keys.map((key) => ({ key, keyIndex: getValue().lastIndexOf(key, caretIndex - 1) })).sort((a, b) => b.keyIndex - a.keyIndex); return keyData; } function getLastSearchText(caretIndex, keyIndex) { if (keyIndex !== -1) { const text = getValue().substring(keyIndex + 1, caretIndex); if (props.allowSpace) { return text.trim(); } if (!/\s/.test(text)) { return text; } } return null; } const caretPosition = ref(null); function updateCaretPosition() { if (currentKey.value) { if (input.isContentEditable) { const rect = window.getSelection().getRangeAt(0).getBoundingClientRect(); const inputRect = input.getBoundingClientRect(); caretPosition.value = { left: rect.left - inputRect.left, top: rect.top - inputRect.top, height: rect.height }; } else { caretPosition.value = getCaretPosition(input, currentKeyIndex); } caretPosition.value.top -= input.scrollTop; if (props.caretHeight) { caretPosition.value.height = props.caretHeight; } else if (isNaN(caretPosition.value.height)) { caretPosition.value.height = 16; } } } function openMenu(key, keyIndex) { if (currentKey.value !== key) { currentKey.value = key; currentKeyIndex = keyIndex; updateCaretPosition(); selectedIndex.value = 0; emit("open", currentKey.value); isMentioning.value = true; } } function closeMenu() { if (currentKey.value != null) { oldKey.value = currentKey.value; currentKey.value = null; isMentioning.value = false; emit("close", oldKey.value); } } function applyMention(itemIndex) { const item = displayedItems.value[itemIndex]; const value = (props.omitKey ? "" : currentKey.value) + String(props.mapInsert ? props.mapInsert(item, currentKey.value) : item.value) + (props.insertSpace ? " " : ""); if (input.isContentEditable) { const range = window.getSelection().getRangeAt(0); range.setStart(range.startContainer, range.startOffset - currentKey.value.length - (lastSearchText ? lastSearchText.length : 0)); range.deleteContents(); range.insertNode(document.createTextNode(value)); range.setStart(range.endContainer, range.endOffset); emitInputEvent("input"); } else { setValue(replaceText(getValue(), searchText.value, value, currentKeyIndex)); setCaretPosition(currentKeyIndex + value.length); } emit("apply", item, currentKey.value, value); closeMenu(); } function replaceText(text, searchString, newText, index) { return text.slice(0, index) + newText + text.slice(index + searchString.length + 1, text.length); } return { el, currentKey, oldKey, caretPosition, displayedItems, selectedIndex, applyMention, isMentioning }; } }); const _hoisted_1 = { key: 0 }; const _hoisted_2 = ["onMouseover", "onMousedown"]; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_VDropdown = resolveComponent("VDropdown"); return openBlock(), createElementBlock("div", { ref: "el", class: normalizeClass(["mentionable", _ctx.$attrs.class]), style: { "position": "relative" } }, [ renderSlot(_ctx.$slots, "default"), createVNode(_component_VDropdown, mergeProps({ ref: "popper" }, { ..._ctx.$attrs, class: void 0 }, { shown: !!_ctx.currentKey, triggers: [], "auto-hide": false, theme: _ctx.theme, class: "popper", style: [{ "position": "absolute" }, _ctx.caretPosition ? { top: `${_ctx.caretPosition.top}px`, left: `${_ctx.caretPosition.left}px` } : {}] }), { popper: withCtx(() => [ !_ctx.displayedItems.length ? (openBlock(), createElementBlock("div", _hoisted_1, [ renderSlot(_ctx.$slots, "no-result", {}, () => [ _cache[0] || (_cache[0] = createTextVNode(" No result ")) ]) ])) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(_ctx.displayedItems, (item, index) => { return openBlock(), createElementBlock("div", { key: index, class: normalizeClass(["mention-item", { "mention-selected": _ctx.selectedIndex === index }]), onMouseover: ($event) => _ctx.selectedIndex = index, onMousedown: ($event) => _ctx.applyMention(index) }, [ renderSlot(_ctx.$slots, `item-${_ctx.currentKey || _ctx.oldKey}`, { item, index }, () => [ renderSlot(_ctx.$slots, "item", { item, index }, () => [ createTextVNode(toDisplayString(item.label || item.value), 1) ]) ]) ], 42, _hoisted_2); }), 128)) ]), default: withCtx(() => [ createElementVNode("div", { style: normalizeStyle(_ctx.caretPosition ? { height: `${_ctx.caretPosition.height}px` } : {}) }, null, 4) ]), _: 3 }, 16, ["shown", "theme", "style"]) ], 2); } var Mentionable = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); function registerComponents(app, prefix) { app.component(`${prefix}mentionable`, Mentionable); app.component(`${prefix}Mentionable`, Mentionable); } function install(app, options2) { const finalOptions = Object.assign({}, { installComponents: true, componentsPrefix: "" }, options2); if (finalOptions.installComponents) { registerComponents(app, finalOptions.componentsPrefix); } } const plugin = { version: "3.0.0", install }; export { Mentionable, plugin as default, install };