You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					162 lines
				
				5.8 KiB
			
		
		
			
		
	
	
					162 lines
				
				5.8 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								import { posToDOMRect, Extension } from '@tiptap/core';
							 | 
						||
| 
								 | 
							
								import { Plugin, PluginKey } from 'prosemirror-state';
							 | 
						||
| 
								 | 
							
								import tippy from 'tippy.js';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class FloatingMenuView {
							 | 
						||
| 
								 | 
							
								    constructor({ editor, element, view, tippyOptions = {}, shouldShow, }) {
							 | 
						||
| 
								 | 
							
								        this.preventHide = false;
							 | 
						||
| 
								 | 
							
								        this.shouldShow = ({ view, state }) => {
							 | 
						||
| 
								 | 
							
								            const { selection } = state;
							 | 
						||
| 
								 | 
							
								            const { $anchor, empty } = selection;
							 | 
						||
| 
								 | 
							
								            const isRootDepth = $anchor.depth === 1;
							 | 
						||
| 
								 | 
							
								            const isEmptyTextBlock = $anchor.parent.isTextblock
							 | 
						||
| 
								 | 
							
								                && !$anchor.parent.type.spec.code
							 | 
						||
| 
								 | 
							
								                && !$anchor.parent.textContent;
							 | 
						||
| 
								 | 
							
								            if (!view.hasFocus()
							 | 
						||
| 
								 | 
							
								                || !empty
							 | 
						||
| 
								 | 
							
								                || !isRootDepth
							 | 
						||
| 
								 | 
							
								                || !isEmptyTextBlock
							 | 
						||
| 
								 | 
							
								                || !this.editor.isEditable) {
							 | 
						||
| 
								 | 
							
								                return false;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return true;
							 | 
						||
| 
								 | 
							
								        };
							 | 
						||
| 
								 | 
							
								        this.mousedownHandler = () => {
							 | 
						||
| 
								 | 
							
								            this.preventHide = true;
							 | 
						||
| 
								 | 
							
								        };
							 | 
						||
| 
								 | 
							
								        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.editor = editor;
							 | 
						||
| 
								 | 
							
								        this.element = element;
							 | 
						||
| 
								 | 
							
								        this.view = view;
							 | 
						||
| 
								 | 
							
								        if (shouldShow) {
							 | 
						||
| 
								 | 
							
								            this.shouldShow = shouldShow;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true });
							 | 
						||
| 
								 | 
							
								        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(editorElement, {
							 | 
						||
| 
								 | 
							
								            duration: 0,
							 | 
						||
| 
								 | 
							
								            getReferenceClientRect: null,
							 | 
						||
| 
								 | 
							
								            content: this.element,
							 | 
						||
| 
								 | 
							
								            interactive: true,
							 | 
						||
| 
								 | 
							
								            trigger: 'manual',
							 | 
						||
| 
								 | 
							
								            placement: 'right',
							 | 
						||
| 
								 | 
							
								            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) {
							 | 
						||
| 
								 | 
							
								        var _a, _b, _c;
							 | 
						||
| 
								 | 
							
								        const { state } = view;
							 | 
						||
| 
								 | 
							
								        const { doc, selection } = state;
							 | 
						||
| 
								 | 
							
								        const { from, to } = selection;
							 | 
						||
| 
								 | 
							
								        const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
							 | 
						||
| 
								 | 
							
								        if (isSame) {
							 | 
						||
| 
								 | 
							
								            return;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        this.createTooltip();
							 | 
						||
| 
								 | 
							
								        const shouldShow = (_a = this.shouldShow) === null || _a === void 0 ? void 0 : _a.call(this, {
							 | 
						||
| 
								 | 
							
								            editor: this.editor,
							 | 
						||
| 
								 | 
							
								            view,
							 | 
						||
| 
								 | 
							
								            state,
							 | 
						||
| 
								 | 
							
								            oldState,
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								        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) || (() => posToDOMRect(view, from, to)),
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								        this.show();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    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.editor.off('focus', this.focusHandler);
							 | 
						||
| 
								 | 
							
								        this.editor.off('blur', this.blurHandler);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								const FloatingMenuPlugin = (options) => {
							 | 
						||
| 
								 | 
							
								    return new Plugin({
							 | 
						||
| 
								 | 
							
								        key: typeof options.pluginKey === 'string'
							 | 
						||
| 
								 | 
							
								            ? new PluginKey(options.pluginKey)
							 | 
						||
| 
								 | 
							
								            : options.pluginKey,
							 | 
						||
| 
								 | 
							
								        view: view => new FloatingMenuView({ view, ...options }),
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const FloatingMenu = Extension.create({
							 | 
						||
| 
								 | 
							
								    name: 'floatingMenu',
							 | 
						||
| 
								 | 
							
								    addOptions() {
							 | 
						||
| 
								 | 
							
								        return {
							 | 
						||
| 
								 | 
							
								            element: null,
							 | 
						||
| 
								 | 
							
								            tippyOptions: {},
							 | 
						||
| 
								 | 
							
								            pluginKey: 'floatingMenu',
							 | 
						||
| 
								 | 
							
								            shouldShow: null,
							 | 
						||
| 
								 | 
							
								        };
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    addProseMirrorPlugins() {
							 | 
						||
| 
								 | 
							
								        if (!this.options.element) {
							 | 
						||
| 
								 | 
							
								            return [];
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        return [
							 | 
						||
| 
								 | 
							
								            FloatingMenuPlugin({
							 | 
						||
| 
								 | 
							
								                pluginKey: this.options.pluginKey,
							 | 
						||
| 
								 | 
							
								                editor: this.editor,
							 | 
						||
| 
								 | 
							
								                element: this.options.element,
							 | 
						||
| 
								 | 
							
								                tippyOptions: this.options.tippyOptions,
							 | 
						||
| 
								 | 
							
								                shouldShow: this.options.shouldShow,
							 | 
						||
| 
								 | 
							
								            }),
							 | 
						||
| 
								 | 
							
								        ];
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export { FloatingMenu, FloatingMenuPlugin, FloatingMenuView, FloatingMenu as default };
							 | 
						||
| 
								 | 
							
								//# sourceMappingURL=tiptap-extension-floating-menu.esm.js.map
							 |