UNPKG

lexical-vue

Version:

An extensible Vue 3 web text-editor based on Lexical.

191 lines (190 loc) 9.3 kB
import { createBlock, createCommentVNode, defineComponent, guardReactiveProps, nextTick, normalizeProps, openBlock, ref, renderSlot, unref, useSlots, watchEffect, withCtx } from "vue"; import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_LOW, createCommand } from "lexical"; import { useLexicalComposer } from "./LexicalComposer.vine.js"; import { LexicalMenu, MenuOption, useDynamicPositioning, useMenuAnchorRef } from "./shared/LexicalMenu.vine.js"; const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'; function getScrollParent(element, includeHidden) { let style = getComputedStyle(element); const excludeStaticParent = 'absolute' === style.position; const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/; if ('fixed' === style.position) return document.body; for(let parent = element; parent = parent.parentElement;){ style = getComputedStyle(parent); if (!excludeStaticParent || 'static' !== style.position) { if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; } } return document.body; } const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND = createCommand('SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND'); function useBasicTypeaheadTriggerMatch(trigger, param) { let { minLength = 1, maxLength = 75, punctuation = PUNCTUATION, allowWhitespace = false } = param; return (text)=>{ const validCharsSuffix = allowWhitespace ? '' : '\\s'; const validChars = "[^".concat(trigger).concat(punctuation).concat(validCharsSuffix, "]"); const TypeaheadTriggerRegex = new RegExp("(^|\\s|\\()(" + "[".concat(trigger, "]") + "((?:".concat(validChars, "){0,").concat(maxLength, "})") + ")$"); const match = TypeaheadTriggerRegex.exec(text); if (null !== match) { const maybeLeadingWhitespace = match[1]; const matchingString = match[3]; if (matchingString.length >= minLength) return { leadOffset: match.index + maybeLeadingWhitespace.length, matchingString, replaceableString: match[2] }; } return null; }; } const TypeaheadMenuPlugin = (()=>{ const __vine = defineComponent({ name: 'TypeaheadMenuPlugin', props: { options: { required: true }, triggerFn: { required: true }, anchorClassName: {}, commandPriority: {}, parent: {}, preselectFirstItem: { type: Boolean }, ignoreEntityBoundary: { type: Boolean } }, emits: [ 'close', 'open', 'queryChange', 'selectOption' ], setup (__props, param) { let { emit: __emit, expose: __expose } = param; const emit = __emit; __expose(); const props = __props; useSlots(); const editor = useLexicalComposer(); const resolution = ref(null); function setResolution(payload) { resolution.value = payload; } const anchorElementRef = useMenuAnchorRef(resolution, setResolution, props.anchorClassName, props.parent); function closeTypeahead() { setResolution(null); if (null !== resolution.value) emit('close'); } function openTypeahead(res) { setResolution(res); if (null === resolution.value) emit('open', res); } function getTextUpToAnchor(selection) { const anchor = selection.anchor; if ('text' !== anchor.type) return null; const anchorNode = anchor.getNode(); if (!anchorNode.isSimpleText()) return null; const anchorOffset = anchor.offset; return anchorNode.getTextContent().slice(0, anchorOffset); } function tryToPositionRange(leadOffset, range, editorWindow) { const domSelection = editorWindow.getSelection(); if (null === domSelection || !domSelection.isCollapsed) return false; const anchorNode = domSelection.anchorNode; const startOffset = leadOffset; const endOffset = domSelection.anchorOffset; if (null == anchorNode || null == endOffset) return false; try { range.setStart(anchorNode, startOffset); range.setEnd(anchorNode, endOffset); } catch (e) { return false; } return true; } function getQueryTextForSearch(editor) { let text = null; editor.getEditorState().read(()=>{ const selection = $getSelection(); if (!$isRangeSelection(selection)) return; text = getTextUpToAnchor(selection); }); return text; } function isSelectionOnEntityBoundary(editor, offset) { if (0 !== offset) return false; return editor.getEditorState().read(()=>{ const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchor = selection.anchor; const anchorNode = anchor.getNode(); const prevSibling = anchorNode.getPreviousSibling(); return $isTextNode(prevSibling) && prevSibling.isTextEntity(); } return false; }); } watchEffect((onInvalidate)=>{ const updateListener = ()=>{ editor.getEditorState().read(()=>{ if (!editor.isEditable()) return void closeTypeahead(); const editorWindow = editor._window || window; const range = editorWindow.document.createRange(); const selection = $getSelection(); const text = getQueryTextForSearch(editor); if (!$isRangeSelection(selection) || !selection.isCollapsed() || null === text || null === range) return void closeTypeahead(); const match = props.triggerFn(text, editor); emit('queryChange', match ? match.matchingString : null); if (null !== match && (props.ignoreEntityBoundary || !isSelectionOnEntityBoundary(editor, match.leadOffset))) { const isRangePositioned = tryToPositionRange(match.leadOffset, range, editorWindow); if (null !== isRangePositioned) return void nextTick(()=>openTypeahead({ getRect: ()=>range.getBoundingClientRect(), match })); } closeTypeahead(); }); }; const removeUpdateListener = editor.registerUpdateListener(updateListener); onInvalidate(removeUpdateListener); }); watchEffect((onInvalidate)=>{ const unregister = editor.registerEditableListener((isEditable)=>{ if (!isEditable) closeTypeahead(); }); onInvalidate(unregister); }); return (_ctx, _cache)=>{ var _ctx_commandPriority; return null !== resolution.value && null !== unref(editor) && null !== unref(anchorElementRef) ? (openBlock(), createBlock(unref(LexicalMenu), { key: 0, "anchor-element-ref": unref(anchorElementRef), editor: unref(editor), resolution: resolution.value, options: _ctx.options, "should-split-node-with-query": true, "command-priority": null != (_ctx_commandPriority = _ctx.commandPriority) ? _ctx_commandPriority : unref(COMMAND_PRIORITY_LOW), close: closeTypeahead, onSelectOption: _cache[0] || (_cache[0] = ($event)=>emit('selectOption', $event)) }, { default: withCtx((slotProps)=>[ renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps))) ]), _: 3 }, 8, [ "anchor-element-ref", "editor", "resolution", "options", "command-priority" ])) : createCommentVNode("", true); }; } }); __vine.__vue_vine = true; return __vine; })(); export { MenuOption, PUNCTUATION, SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, TypeaheadMenuPlugin, getScrollParent, useBasicTypeaheadTriggerMatch, useDynamicPositioning };