'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var core = require('@tiptap/core'); var state = require('@tiptap/pm/state'); var tippy = require('tippy.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var tippy__default = /*#__PURE__*/_interopDefaultLegacy(tippy); class BubbleMenuView { constructor({ editor, element, view, tippyOptions = {}, updateDelay = 250, shouldShow, }) { this.preventHide = false; this.shouldShow = ({ view, state, from, to, }) => { const { doc, selection } = state; const { empty } = selection; // Sometime check for `empty` is not enough. // Doubleclick an empty paragraph returns a node size of 2. // So we check also for an empty text size. const isEmptyTextBlock = !doc.textBetween(from, to).length && core.isTextSelection(state.selection); // When clicking on a element inside the bubble menu the editor "blur" event // is called and the bubble menu item is focussed. In this case we should // consider the menu as part of the editor and keep showing the menu const isChildOfMenu = this.element.contains(document.activeElement); const hasEditorFocus = view.hasFocus() || isChildOfMenu; if (!hasEditorFocus || empty || isEmptyTextBlock || !this.editor.isEditable) { return false; } return true; }; this.mousedownHandler = () => { this.preventHide = true; }; this.dragstartHandler = () => { this.hide(); }; this.focusHandler = () => { // we use `setTimeout` to make sure `selection` is already updated setTimeout(() => this.update(this.editor.view)); }; this.blurHandler = ({ event }) => { var _a; if (this.preventHide) { this.preventHide = false; return; } if ((event === null || event === void 0 ? void 0 : event.relatedTarget) && ((_a = this.element.parentNode) === null || _a === void 0 ? void 0 : _a.contains(event.relatedTarget))) { return; } this.hide(); }; this.tippyBlurHandler = (event) => { this.blurHandler({ event }); }; this.handleDebouncedUpdate = (view, oldState) => { const selectionChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.selection.eq(view.state.selection)); const docChanged = !(oldState === null || oldState === void 0 ? void 0 : 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) => { var _a, _b, _c; const { state, composing } = view; const { selection } = state; const isSame = !selectionChanged && !docChanged; if (composing || isSame) { return; } this.createTooltip(); // support for CellSelections 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 = (_a = this.shouldShow) === null || _a === void 0 ? void 0 : _a.call(this, { editor: this.editor, view, state, oldState, from, to, }); if (!shouldShow) { this.hide(); return; } (_b = this.tippy) === null || _b === void 0 ? void 0 : _b.setProps({ getReferenceClientRect: ((_c = this.tippyOptions) === null || _c === void 0 ? void 0 : _c.getReferenceClientRect) || (() => { if (core.isNodeSelection(state.selection)) { let node = view.nodeDOM(from); const nodeViewWrapper = node.dataset.nodeViewWrapper ? node : node.querySelector('[data-node-view-wrapper]'); if (nodeViewWrapper) { node = nodeViewWrapper.firstChild; } if (node) { return node.getBoundingClientRect(); } } return core.posToDOMRect(view, from, to); }), }); this.show(); }; this.editor = editor; this.element = element; this.view = view; this.updateDelay = updateDelay; if (shouldShow) { this.shouldShow = shouldShow; } this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true }); this.view.dom.addEventListener('dragstart', this.dragstartHandler); this.editor.on('focus', this.focusHandler); this.editor.on('blur', this.blurHandler); this.tippyOptions = tippyOptions; // Detaches menu content from its current parent this.element.remove(); this.element.style.visibility = 'visible'; } createTooltip() { const { element: editorElement } = this.editor.options; const editorIsAttached = !!editorElement.parentElement; if (this.tippy || !editorIsAttached) { return; } this.tippy = tippy__default["default"](editorElement, { duration: 0, getReferenceClientRect: null, content: this.element, interactive: true, trigger: 'manual', placement: 'top', hideOnClick: 'toggle', ...this.tippyOptions, }); // maybe we have to hide tippy on its own blur event as well if (this.tippy.popper.firstChild) { this.tippy.popper.firstChild.addEventListener('blur', this.tippyBlurHandler); } } update(view, oldState) { const { state } = view; const hasValidSelection = state.selection.$from.pos !== state.selection.$to.pos; if (this.updateDelay > 0 && hasValidSelection) { this.handleDebouncedUpdate(view, oldState); return; } const selectionChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.selection.eq(view.state.selection)); const docChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.doc.eq(view.state.doc)); this.updateHandler(view, selectionChanged, docChanged, oldState); } show() { var _a; (_a = this.tippy) === null || _a === void 0 ? void 0 : _a.show(); } hide() { var _a; (_a = this.tippy) === null || _a === void 0 ? void 0 : _a.hide(); } destroy() { var _a, _b; if ((_a = this.tippy) === null || _a === void 0 ? void 0 : _a.popper.firstChild) { this.tippy.popper.firstChild.removeEventListener('blur', this.tippyBlurHandler); } (_b = this.tippy) === null || _b === void 0 ? void 0 : _b.destroy(); this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true }); this.view.dom.removeEventListener('dragstart', this.dragstartHandler); this.editor.off('focus', this.focusHandler); this.editor.off('blur', this.blurHandler); } } const BubbleMenuPlugin = (options) => { return new state.Plugin({ key: typeof options.pluginKey === 'string' ? new state.PluginKey(options.pluginKey) : options.pluginKey, view: view => new BubbleMenuView({ view, ...options }), }); }; const BubbleMenu = core.Extension.create({ name: 'bubbleMenu', addOptions() { return { element: null, tippyOptions: {}, pluginKey: 'bubbleMenu', updateDelay: undefined, shouldShow: null, }; }, addProseMirrorPlugins() { if (!this.options.element) { return []; } return [ BubbleMenuPlugin({ pluginKey: this.options.pluginKey, editor: this.editor, element: this.options.element, tippyOptions: this.options.tippyOptions, updateDelay: this.options.updateDelay, shouldShow: this.options.shouldShow, }), ]; }, }); exports.BubbleMenu = BubbleMenu; exports.BubbleMenuPlugin = BubbleMenuPlugin; exports.BubbleMenuView = BubbleMenuView; exports["default"] = BubbleMenu; //# sourceMappingURL=index.cjs.map