UNPKG

rhino-editor

Version:

A custom element wrapped rich text editor

218 lines (216 loc) 6.42 kB
// src/exports/extensions/bubble-menu.ts import { Extension } from "@tiptap/core"; import { isNodeSelection, isTextSelection, posToDOMRect } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; function findNodeViewAnchor({ view, from, editor }) { let node = view.nodeDOM(from); if (editor.isActive("attachment-figure") || editor.isActive("previewable-attachment-figure")) { const figcaption = node.querySelector("figcaption"); node = figcaption || node; } let nodeViewWrapper = node.dataset.nodeViewWrapper ? node : node.querySelector("[data-node-view-wrapper]"); if (nodeViewWrapper) { node = nodeViewWrapper.firstChild; } return node; } var BubbleMenuView = class { constructor({ editor, element, view, // tippyOptions = {}, updateDelay = 250, shouldShow, determineNodeViewAnchor }) { this.preventHide = false; this.shouldShow = ({ view, state, from, to }) => { const { doc, selection } = state; const { empty } = selection; const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection); const isChildOfMenu = this.element.matches(":focus-within"); const hasEditorFocus = view.hasFocus() || isChildOfMenu; if (!hasEditorFocus || empty || isEmptyTextBlock || !this.editor.isEditable) { return false; } return true; }; this.determineNodeViewAnchor = ({ view, from }) => { return findNodeViewAnchor({ view, from, editor: this.editor }); }; this.dragstartHandler = () => { }; this.focusHandler = () => { setTimeout(() => this.update(this.editor.view)); }; this.blurHandler = () => { setTimeout(() => { if (!this.element.matches(":focus-within") && !this.view.dom.matches(":focus-within")) { this.hide(); } }); }; this.handleDebouncedUpdate = (view, oldState) => { const selectionChanged = !oldState?.selection.eq(view.state.selection); const docChanged = !oldState?.doc.eq(view.state.doc); if (!selectionChanged && !docChanged) { return; } if (this.updateDebounceTimer) { clearTimeout(this.updateDebounceTimer); } this.updateDebounceTimer = window.setTimeout(() => { this.updateHandler(view, selectionChanged, docChanged, oldState); }, this.updateDelay); }; this.updateHandler = (view, selectionChanged, docChanged, oldState) => { const { state, composing } = view; const { selection } = state; const isSame = !selectionChanged && !docChanged; if (composing || isSame) { return; } const { ranges } = selection; const from = Math.min(...ranges.map((range) => range.$from.pos)); const to = Math.max(...ranges.map((range) => range.$to.pos)); const shouldShow = this.shouldShow?.({ editor: this.editor, view, state, oldState, from, to }); if (!shouldShow) { this.hide(); return; } let clientRect = null; if (isNodeSelection(state.selection)) { const node = this.determineNodeViewAnchor?.({ editor: this.editor, view, state, oldState, from, to }) || view.nodeDOM(from); if (node) { clientRect = () => { const rect = node.getBoundingClientRect(); return rect; }; } } else { clientRect = () => { const rect = posToDOMRect(view, from, to); rect.x = rect.x - rect.width / 2; return rect; }; } if (clientRect) { this.show(clientRect); } }; this.editor = editor; this.element = element; this.view = view; this.updateDelay = updateDelay; if (shouldShow) { this.shouldShow = shouldShow; } if (determineNodeViewAnchor) { this.determineNodeViewAnchor = determineNodeViewAnchor; } this.view.dom.addEventListener("dragstart", this.dragstartHandler); this.editor.on("focus", this.focusHandler); this.editor.on("blur", this.blurHandler); this.element.addEventListener("focusout", this.blurHandler); } destroy() { this.view.dom.removeEventListener("dragstart", this.dragstartHandler); this.editor.off("focus", this.focusHandler); this.editor.off("blur", this.blurHandler); this.element.removeEventListener("focusout", this.blurHandler); } update(view, oldState) { const { state } = view; const hasValidSelection = state.selection.from !== state.selection.to; if (this.updateDelay > 0 && hasValidSelection) { this.handleDebouncedUpdate(view, oldState); return; } const selectionChanged = !oldState?.selection.eq(view.state.selection); const docChanged = !oldState?.doc.eq(view.state.doc); this.updateHandler(view, selectionChanged, docChanged, oldState); } show(clientRect) { const evt = new Event("rhino-bubble-menu-show", { bubbles: true, composed: true, cancelable: true }); evt.clientRect = clientRect; this.element.dispatchEvent(evt); } hide() { const evt = new Event("rhino-bubble-menu-hide", { bubbles: true, composed: true }); this.element.dispatchEvent(evt); } }; var BubbleMenuPlugin = (options) => { return new Plugin({ key: typeof options.pluginKey === "string" ? new PluginKey(options.pluginKey) : options.pluginKey, view: (view) => new BubbleMenuView({ view, ...options }) }); }; var BubbleMenuExtension = Extension.create({ name: "rhino-bubble-menu", addOptions() { return { element: null, pluginKey: "rhino-bubble-menu", updateDelay: void 0, shouldShow: null }; }, addProseMirrorPlugins() { if (!this.options.element) { return []; } return [ BubbleMenuPlugin({ pluginKey: this.options.pluginKey, editor: this.editor, element: this.options.element, updateDelay: this.options.updateDelay, shouldShow: this.options.shouldShow, determineNodeViewAnchor: this.options.determineNodeViewAnchor }) ]; } }); export { findNodeViewAnchor, BubbleMenuView, BubbleMenuPlugin, BubbleMenuExtension }; //# sourceMappingURL=chunk-4EN52UIW.js.map