rhino-editor
Version:
A custom element wrapped rich text editor
218 lines (216 loc) • 6.42 kB
JavaScript
// 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