@tiptap/extension-bubble-menu
Advanced tools
Comparing version 2.5.8 to 3.0.0-next.0
@@ -1,8 +0,48 @@ | ||
import { isTextSelection, isNodeSelection, posToDOMRect, Extension } from '@tiptap/core'; | ||
import { isTextSelection, posToDOMRect, Extension } from '@tiptap/core'; | ||
import { flip, shift, offset, arrow, size, autoPlacement, hide, inline, computePosition } from '@floating-ui/dom'; | ||
import { Plugin, PluginKey } from '@tiptap/pm/state'; | ||
import tippy from 'tippy.js'; | ||
class BubbleMenuView { | ||
constructor({ editor, element, view, tippyOptions = {}, updateDelay = 250, shouldShow, }) { | ||
get middlewares() { | ||
const middlewares = []; | ||
if (this.floatingUIOptions.flip) { | ||
middlewares.push(flip(typeof this.floatingUIOptions.flip !== 'boolean' ? this.floatingUIOptions.flip : undefined)); | ||
} | ||
if (this.floatingUIOptions.shift) { | ||
middlewares.push(shift(typeof this.floatingUIOptions.shift !== 'boolean' ? this.floatingUIOptions.shift : undefined)); | ||
} | ||
if (this.floatingUIOptions.offset) { | ||
middlewares.push(offset(typeof this.floatingUIOptions.offset !== 'boolean' ? this.floatingUIOptions.offset : undefined)); | ||
} | ||
if (this.floatingUIOptions.arrow) { | ||
middlewares.push(arrow(this.floatingUIOptions.arrow)); | ||
} | ||
if (this.floatingUIOptions.size) { | ||
middlewares.push(size(typeof this.floatingUIOptions.size !== 'boolean' ? this.floatingUIOptions.size : undefined)); | ||
} | ||
if (this.floatingUIOptions.autoPlacement) { | ||
middlewares.push(autoPlacement(typeof this.floatingUIOptions.autoPlacement !== 'boolean' ? this.floatingUIOptions.autoPlacement : undefined)); | ||
} | ||
if (this.floatingUIOptions.hide) { | ||
middlewares.push(hide(typeof this.floatingUIOptions.hide !== 'boolean' ? this.floatingUIOptions.hide : undefined)); | ||
} | ||
if (this.floatingUIOptions.inline) { | ||
middlewares.push(inline(typeof this.floatingUIOptions.inline !== 'boolean' ? this.floatingUIOptions.inline : undefined)); | ||
} | ||
return middlewares; | ||
} | ||
constructor({ editor, element, view, updateDelay = 250, resizeDelay = 60, shouldShow, options, }) { | ||
this.preventHide = false; | ||
this.floatingUIOptions = { | ||
strategy: 'absolute', | ||
placement: 'top', | ||
offset: 8, | ||
flip: {}, | ||
shift: {}, | ||
arrow: false, | ||
size: false, | ||
autoPlacement: false, | ||
hide: false, | ||
inline: false, | ||
}; | ||
this.shouldShow = ({ view, state, from, to, }) => { | ||
@@ -46,5 +86,2 @@ const { doc, selection } = state; | ||
}; | ||
this.tippyBlurHandler = (event) => { | ||
this.blurHandler({ event }); | ||
}; | ||
this.handleDebouncedUpdate = (view, oldState) => { | ||
@@ -64,5 +101,3 @@ const selectionChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.selection.eq(view.state.selection)); | ||
this.updateHandler = (view, selectionChanged, docChanged, oldState) => { | ||
var _a, _b, _c; | ||
const { state, composing } = view; | ||
const { selection } = state; | ||
const { composing } = view; | ||
const isSame = !selectionChanged && !docChanged; | ||
@@ -72,15 +107,3 @@ if (composing || isSame) { | ||
} | ||
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, | ||
}); | ||
const shouldShow = this.getShouldShow(oldState); | ||
if (!shouldShow) { | ||
@@ -90,18 +113,3 @@ this.hide(); | ||
} | ||
(_b = this.tippy) === null || _b === void 0 ? void 0 : _b.setProps({ | ||
getReferenceClientRect: ((_c = this.tippyOptions) === null || _c === void 0 ? void 0 : _c.getReferenceClientRect) | ||
|| (() => { | ||
if (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 posToDOMRect(view, from, to); | ||
}), | ||
}); | ||
this.updatePosition(); | ||
this.show(); | ||
@@ -113,2 +121,7 @@ }; | ||
this.updateDelay = updateDelay; | ||
this.resizeDelay = resizeDelay; | ||
this.floatingUIOptions = { | ||
...this.floatingUIOptions, | ||
...options, | ||
}; | ||
if (shouldShow) { | ||
@@ -121,28 +134,27 @@ this.shouldShow = shouldShow; | ||
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: 'top', | ||
hideOnClick: 'toggle', | ||
...this.tippyOptions, | ||
window.addEventListener('resize', () => { | ||
if (this.resizeDebounceTimer) { | ||
clearTimeout(this.resizeDebounceTimer); | ||
} | ||
this.resizeDebounceTimer = window.setTimeout(() => { | ||
this.updatePosition(); | ||
}, this.resizeDelay); | ||
}); | ||
// 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); | ||
this.update(view, view.state); | ||
if (this.getShouldShow()) { | ||
this.show(); | ||
} | ||
} | ||
updatePosition() { | ||
const { selection } = this.editor.state; | ||
const virtualElement = { | ||
getBoundingClientRect: () => posToDOMRect(this.view, selection.from, selection.to), | ||
}; | ||
computePosition(virtualElement, this.element, { placement: this.floatingUIOptions.placement, strategy: this.floatingUIOptions.strategy, middleware: this.middlewares }).then(({ x, y, strategy }) => { | ||
this.element.style.width = 'max-content'; | ||
this.element.style.position = strategy; | ||
this.element.style.left = `${x}px`; | ||
this.element.style.top = `${y}px`; | ||
}); | ||
} | ||
update(view, oldState) { | ||
@@ -159,16 +171,33 @@ const { state } = view; | ||
} | ||
show() { | ||
getShouldShow(oldState) { | ||
var _a; | ||
(_a = this.tippy) === null || _a === void 0 ? void 0 : _a.show(); | ||
const { state } = this.view; | ||
const { selection } = state; | ||
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: this.view, | ||
state, | ||
oldState, | ||
from, | ||
to, | ||
}); | ||
return shouldShow; | ||
} | ||
show() { | ||
this.element.style.visibility = 'visible'; | ||
this.element.style.opacity = '1'; | ||
// attach from body | ||
document.body.appendChild(this.element); | ||
} | ||
hide() { | ||
var _a; | ||
(_a = this.tippy) === null || _a === void 0 ? void 0 : _a.hide(); | ||
this.element.style.visibility = 'hidden'; | ||
this.element.style.opacity = '0'; | ||
// remove from body | ||
this.element.remove(); | ||
} | ||
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.hide(); | ||
this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true }); | ||
@@ -196,3 +225,2 @@ this.view.dom.removeEventListener('dragstart', this.dragstartHandler); | ||
element: null, | ||
tippyOptions: {}, | ||
pluginKey: 'bubbleMenu', | ||
@@ -212,3 +240,2 @@ updateDelay: undefined, | ||
element: this.options.element, | ||
tippyOptions: this.options.tippyOptions, | ||
updateDelay: this.options.updateDelay, | ||
@@ -215,0 +242,0 @@ shouldShow: this.options.shouldShow, |
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('@tiptap/pm/state'), require('tippy.js')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', '@tiptap/pm/state', 'tippy.js'], factory) : | ||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-bubble-menu"] = {}, global.core, global.state, global.tippy)); | ||
})(this, (function (exports, core, state, tippy) { 'use strict'; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('@floating-ui/dom'), require('@tiptap/pm/state')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', '@floating-ui/dom', '@tiptap/pm/state'], factory) : | ||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-bubble-menu"] = {}, global.core, global.dom, global.state)); | ||
})(this, (function (exports, core, dom, state) { 'use strict'; | ||
class BubbleMenuView { | ||
constructor({ editor, element, view, tippyOptions = {}, updateDelay = 250, shouldShow, }) { | ||
get middlewares() { | ||
const middlewares = []; | ||
if (this.floatingUIOptions.flip) { | ||
middlewares.push(dom.flip(typeof this.floatingUIOptions.flip !== 'boolean' ? this.floatingUIOptions.flip : undefined)); | ||
} | ||
if (this.floatingUIOptions.shift) { | ||
middlewares.push(dom.shift(typeof this.floatingUIOptions.shift !== 'boolean' ? this.floatingUIOptions.shift : undefined)); | ||
} | ||
if (this.floatingUIOptions.offset) { | ||
middlewares.push(dom.offset(typeof this.floatingUIOptions.offset !== 'boolean' ? this.floatingUIOptions.offset : undefined)); | ||
} | ||
if (this.floatingUIOptions.arrow) { | ||
middlewares.push(dom.arrow(this.floatingUIOptions.arrow)); | ||
} | ||
if (this.floatingUIOptions.size) { | ||
middlewares.push(dom.size(typeof this.floatingUIOptions.size !== 'boolean' ? this.floatingUIOptions.size : undefined)); | ||
} | ||
if (this.floatingUIOptions.autoPlacement) { | ||
middlewares.push(dom.autoPlacement(typeof this.floatingUIOptions.autoPlacement !== 'boolean' ? this.floatingUIOptions.autoPlacement : undefined)); | ||
} | ||
if (this.floatingUIOptions.hide) { | ||
middlewares.push(dom.hide(typeof this.floatingUIOptions.hide !== 'boolean' ? this.floatingUIOptions.hide : undefined)); | ||
} | ||
if (this.floatingUIOptions.inline) { | ||
middlewares.push(dom.inline(typeof this.floatingUIOptions.inline !== 'boolean' ? this.floatingUIOptions.inline : undefined)); | ||
} | ||
return middlewares; | ||
} | ||
constructor({ editor, element, view, updateDelay = 250, resizeDelay = 60, shouldShow, options, }) { | ||
this.preventHide = false; | ||
this.floatingUIOptions = { | ||
strategy: 'absolute', | ||
placement: 'top', | ||
offset: 8, | ||
flip: {}, | ||
shift: {}, | ||
arrow: false, | ||
size: false, | ||
autoPlacement: false, | ||
hide: false, | ||
inline: false, | ||
}; | ||
this.shouldShow = ({ view, state, from, to, }) => { | ||
@@ -48,5 +88,2 @@ const { doc, selection } = state; | ||
}; | ||
this.tippyBlurHandler = (event) => { | ||
this.blurHandler({ event }); | ||
}; | ||
this.handleDebouncedUpdate = (view, oldState) => { | ||
@@ -66,5 +103,3 @@ const selectionChanged = !(oldState === null || oldState === void 0 ? void 0 : oldState.selection.eq(view.state.selection)); | ||
this.updateHandler = (view, selectionChanged, docChanged, oldState) => { | ||
var _a, _b, _c; | ||
const { state, composing } = view; | ||
const { selection } = state; | ||
const { composing } = view; | ||
const isSame = !selectionChanged && !docChanged; | ||
@@ -74,15 +109,3 @@ if (composing || isSame) { | ||
} | ||
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, | ||
}); | ||
const shouldShow = this.getShouldShow(oldState); | ||
if (!shouldShow) { | ||
@@ -92,18 +115,3 @@ this.hide(); | ||
} | ||
(_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.updatePosition(); | ||
this.show(); | ||
@@ -115,2 +123,7 @@ }; | ||
this.updateDelay = updateDelay; | ||
this.resizeDelay = resizeDelay; | ||
this.floatingUIOptions = { | ||
...this.floatingUIOptions, | ||
...options, | ||
}; | ||
if (shouldShow) { | ||
@@ -123,28 +136,27 @@ this.shouldShow = shouldShow; | ||
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: 'top', | ||
hideOnClick: 'toggle', | ||
...this.tippyOptions, | ||
window.addEventListener('resize', () => { | ||
if (this.resizeDebounceTimer) { | ||
clearTimeout(this.resizeDebounceTimer); | ||
} | ||
this.resizeDebounceTimer = window.setTimeout(() => { | ||
this.updatePosition(); | ||
}, this.resizeDelay); | ||
}); | ||
// 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); | ||
this.update(view, view.state); | ||
if (this.getShouldShow()) { | ||
this.show(); | ||
} | ||
} | ||
updatePosition() { | ||
const { selection } = this.editor.state; | ||
const virtualElement = { | ||
getBoundingClientRect: () => core.posToDOMRect(this.view, selection.from, selection.to), | ||
}; | ||
dom.computePosition(virtualElement, this.element, { placement: this.floatingUIOptions.placement, strategy: this.floatingUIOptions.strategy, middleware: this.middlewares }).then(({ x, y, strategy }) => { | ||
this.element.style.width = 'max-content'; | ||
this.element.style.position = strategy; | ||
this.element.style.left = `${x}px`; | ||
this.element.style.top = `${y}px`; | ||
}); | ||
} | ||
update(view, oldState) { | ||
@@ -161,16 +173,33 @@ const { state } = view; | ||
} | ||
show() { | ||
getShouldShow(oldState) { | ||
var _a; | ||
(_a = this.tippy) === null || _a === void 0 ? void 0 : _a.show(); | ||
const { state } = this.view; | ||
const { selection } = state; | ||
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: this.view, | ||
state, | ||
oldState, | ||
from, | ||
to, | ||
}); | ||
return shouldShow; | ||
} | ||
show() { | ||
this.element.style.visibility = 'visible'; | ||
this.element.style.opacity = '1'; | ||
// attach from body | ||
document.body.appendChild(this.element); | ||
} | ||
hide() { | ||
var _a; | ||
(_a = this.tippy) === null || _a === void 0 ? void 0 : _a.hide(); | ||
this.element.style.visibility = 'hidden'; | ||
this.element.style.opacity = '0'; | ||
// remove from body | ||
this.element.remove(); | ||
} | ||
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.hide(); | ||
this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true }); | ||
@@ -198,3 +227,2 @@ this.view.dom.removeEventListener('dragstart', this.dragstartHandler); | ||
element: null, | ||
tippyOptions: {}, | ||
pluginKey: 'bubbleMenu', | ||
@@ -214,3 +242,2 @@ updateDelay: undefined, | ||
element: this.options.element, | ||
tippyOptions: this.options.tippyOptions, | ||
updateDelay: this.options.updateDelay, | ||
@@ -217,0 +244,0 @@ shouldShow: this.options.shouldShow, |
@@ -341,4 +341,4 @@ import { Plugin, Transaction } from '@tiptap/pm/state'; | ||
static create<O = any, S = any>(config?: Partial<ExtensionConfig<O, S>>): Extension<O, S>; | ||
configure(options?: Partial<Options>): Extension<any, any>; | ||
configure(options?: Partial<Options>): Extension<Options, Storage>; | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>>): Extension<ExtendedOptions, ExtendedStorage>; | ||
} |
@@ -445,3 +445,3 @@ import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model'; | ||
static create<O = any, S = any>(config?: Partial<MarkConfig<O, S>>): Mark<O, S>; | ||
configure(options?: Partial<Options>): Mark<any, any>; | ||
configure(options?: Partial<Options>): Mark<Options, Storage>; | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>>): Mark<ExtendedOptions, ExtendedStorage>; | ||
@@ -448,0 +448,0 @@ static handleExit({ editor, mark }: { |
@@ -609,4 +609,4 @@ import { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model'; | ||
static create<O = any, S = any>(config?: Partial<NodeConfig<O, S>>): Node<O, S>; | ||
configure(options?: Partial<Options>): Node<any, any>; | ||
configure(options?: Partial<Options>): Node<Options, Storage>; | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>>): Node<ExtendedOptions, ExtendedStorage>; | ||
} |
@@ -1,1 +0,1 @@ | ||
export declare const style = ".ProseMirror {\n position: relative;\n}\n\n.ProseMirror {\n word-wrap: break-word;\n white-space: pre-wrap;\n white-space: break-spaces;\n -webkit-font-variant-ligatures: none;\n font-variant-ligatures: none;\n font-feature-settings: \"liga\" 0; /* the above doesn't seem to work in Edge */\n}\n\n.ProseMirror [contenteditable=\"false\"] {\n white-space: normal;\n}\n\n.ProseMirror [contenteditable=\"false\"] [contenteditable=\"true\"] {\n white-space: pre-wrap;\n}\n\n.ProseMirror pre {\n white-space: pre-wrap;\n}\n\nimg.ProseMirror-separator {\n display: inline !important;\n border: none !important;\n margin: 0 !important;\n width: 1px !important;\n height: 1px !important;\n}\n\n.ProseMirror-gapcursor {\n display: none;\n pointer-events: none;\n position: absolute;\n margin: 0;\n}\n\n.ProseMirror-gapcursor:after {\n content: \"\";\n display: block;\n position: absolute;\n top: -2px;\n width: 20px;\n border-top: 1px solid black;\n animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;\n}\n\n@keyframes ProseMirror-cursor-blink {\n to {\n visibility: hidden;\n }\n}\n\n.ProseMirror-hideselection *::selection {\n background: transparent;\n}\n\n.ProseMirror-hideselection *::-moz-selection {\n background: transparent;\n}\n\n.ProseMirror-hideselection * {\n caret-color: transparent;\n}\n\n.ProseMirror-focused .ProseMirror-gapcursor {\n display: block;\n}\n\n.tippy-box[data-animation=fade][data-state=hidden] {\n opacity: 0\n}"; | ||
export declare const style = ".ProseMirror {\n position: relative;\n}\n\n.ProseMirror {\n word-wrap: break-word;\n white-space: pre-wrap;\n white-space: break-spaces;\n -webkit-font-variant-ligatures: none;\n font-variant-ligatures: none;\n font-feature-settings: \"liga\" 0; /* the above doesn't seem to work in Edge */\n}\n\n.ProseMirror [contenteditable=\"false\"] {\n white-space: normal;\n}\n\n.ProseMirror [contenteditable=\"false\"] [contenteditable=\"true\"] {\n white-space: pre-wrap;\n}\n\n.ProseMirror pre {\n white-space: pre-wrap;\n}\n\nimg.ProseMirror-separator {\n display: inline !important;\n border: none !important;\n margin: 0 !important;\n width: 1px !important;\n height: 1px !important;\n}\n\n.ProseMirror-gapcursor {\n display: none;\n pointer-events: none;\n position: absolute;\n margin: 0;\n}\n\n.ProseMirror-gapcursor:after {\n content: \"\";\n display: block;\n position: absolute;\n top: -2px;\n width: 20px;\n border-top: 1px solid black;\n animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;\n}\n\n@keyframes ProseMirror-cursor-blink {\n to {\n visibility: hidden;\n }\n}\n\n.ProseMirror-hideselection *::selection {\n background: transparent;\n}\n\n.ProseMirror-hideselection *::-moz-selection {\n background: transparent;\n}\n\n.ProseMirror-hideselection * {\n caret-color: transparent;\n}\n\n.ProseMirror-focused .ProseMirror-gapcursor {\n display: block;\n}"; |
@@ -0,5 +1,5 @@ | ||
import { type ArrowOptions, type AutoPlacementOptions, type FlipOptions, type HideOptions, type InlineOptions, type OffsetOptions, type Placement, type ShiftOptions, type SizeOptions, type Strategy } from '@floating-ui/dom'; | ||
import { Editor } from '@tiptap/core'; | ||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'; | ||
import { EditorView } from '@tiptap/pm/view'; | ||
import { Instance, Props } from 'tippy.js'; | ||
export interface BubbleMenuPluginProps { | ||
@@ -23,7 +23,2 @@ /** | ||
/** | ||
* The options for the tippy.js instance. | ||
* @see https://atomiks.github.io/tippyjs/v6/all-props/ | ||
*/ | ||
tippyOptions?: Partial<Props>; | ||
/** | ||
* The delay in milliseconds before the menu should be updated. | ||
@@ -36,6 +31,13 @@ * This can be useful to prevent performance issues. | ||
/** | ||
* The delay in milliseconds before the menu position should be updated on window resize. | ||
* This can be useful to prevent performance issues. | ||
* @type {number} | ||
* @default 60 | ||
*/ | ||
resizeDelay?: number; | ||
/** | ||
* A function that determines whether the menu should be shown or not. | ||
* If this function returns `false`, the menu will be hidden, otherwise it will be shown. | ||
*/ | ||
shouldShow?: ((props: { | ||
shouldShow: ((props: { | ||
editor: Editor; | ||
@@ -48,2 +50,17 @@ view: EditorView; | ||
}) => boolean) | null; | ||
/** | ||
* FloatingUI options. | ||
*/ | ||
options?: { | ||
strategy?: Strategy; | ||
placement?: Placement; | ||
offset?: OffsetOptions | boolean; | ||
flip?: FlipOptions | boolean; | ||
shift?: ShiftOptions | boolean; | ||
arrow?: ArrowOptions | false; | ||
size?: SizeOptions | boolean; | ||
autoPlacement?: AutoPlacementOptions | boolean; | ||
hide?: HideOptions | boolean; | ||
inline?: InlineOptions | boolean; | ||
}; | ||
} | ||
@@ -58,8 +75,14 @@ export type BubbleMenuViewProps = BubbleMenuPluginProps & { | ||
preventHide: boolean; | ||
tippy: Instance | undefined; | ||
tippyOptions?: Partial<Props>; | ||
updateDelay: number; | ||
resizeDelay: number; | ||
private updateDebounceTimer; | ||
private resizeDebounceTimer; | ||
private floatingUIOptions; | ||
shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null>; | ||
constructor({ editor, element, view, tippyOptions, updateDelay, shouldShow, }: BubbleMenuViewProps); | ||
get middlewares(): { | ||
name: string; | ||
options?: any; | ||
fn: (state: import("@floating-ui/dom").MiddlewareState) => import("@floating-ui/core").MiddlewareReturn | Promise<import("@floating-ui/core").MiddlewareReturn>; | ||
}[]; | ||
constructor({ editor, element, view, updateDelay, resizeDelay, shouldShow, options, }: BubbleMenuViewProps); | ||
mousedownHandler: () => void; | ||
@@ -71,6 +94,6 @@ dragstartHandler: () => void; | ||
}) => void; | ||
tippyBlurHandler: (event: FocusEvent) => void; | ||
createTooltip(): void; | ||
updatePosition(): void; | ||
update(view: EditorView, oldState?: EditorState): void; | ||
handleDebouncedUpdate: (view: EditorView, oldState?: EditorState) => void; | ||
getShouldShow(oldState?: EditorState): boolean; | ||
updateHandler: (view: EditorView, selectionChanged: boolean, docChanged: boolean, oldState?: EditorState) => void; | ||
@@ -77,0 +100,0 @@ show(): void; |
{ | ||
"name": "@tiptap/extension-bubble-menu", | ||
"description": "bubble-menu extension for tiptap", | ||
"version": "2.5.8", | ||
"version": "3.0.0-next.0", | ||
"homepage": "https://tiptap.dev", | ||
@@ -31,5 +31,2 @@ "keywords": [ | ||
], | ||
"dependencies": { | ||
"tippy.js": "^6.3.7" | ||
}, | ||
"repository": { | ||
@@ -42,8 +39,10 @@ "type": "git", | ||
"devDependencies": { | ||
"@tiptap/core": "^2.5.8", | ||
"@tiptap/pm": "^2.5.8" | ||
"@floating-ui/dom": "^1.0.0", | ||
"@tiptap/core": "^3.0.0-next.0", | ||
"@tiptap/pm": "^3.0.0-next.0" | ||
}, | ||
"peerDependencies": { | ||
"@tiptap/core": "^2.5.8", | ||
"@tiptap/pm": "^2.5.8" | ||
"@floating-ui/dom": "^1.0.0", | ||
"@tiptap/core": "^3.0.0-next.0", | ||
"@tiptap/pm": "^3.0.0-next.0" | ||
}, | ||
@@ -50,0 +49,0 @@ "scripts": { |
import { | ||
Editor, isNodeSelection, isTextSelection, posToDOMRect, | ||
type ArrowOptions, | ||
type AutoPlacementOptions, | ||
type FlipOptions, | ||
type HideOptions, | ||
type InlineOptions, | ||
type Middleware, type OffsetOptions, type Placement, type ShiftOptions, type SizeOptions, type Strategy, arrow, autoPlacement, computePosition, flip, hide, inline, offset, shift, | ||
size, | ||
} from '@floating-ui/dom' | ||
import { | ||
Editor, isTextSelection, posToDOMRect, | ||
} from '@tiptap/core' | ||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state' | ||
import { EditorView } from '@tiptap/pm/view' | ||
import tippy, { Instance, Props } from 'tippy.js' | ||
@@ -29,8 +37,2 @@ export interface BubbleMenuPluginProps { | ||
/** | ||
* The options for the tippy.js instance. | ||
* @see https://atomiks.github.io/tippyjs/v6/all-props/ | ||
*/ | ||
tippyOptions?: Partial<Props> | ||
/** | ||
* The delay in milliseconds before the menu should be updated. | ||
@@ -44,6 +46,14 @@ * This can be useful to prevent performance issues. | ||
/** | ||
* The delay in milliseconds before the menu position should be updated on window resize. | ||
* This can be useful to prevent performance issues. | ||
* @type {number} | ||
* @default 60 | ||
*/ | ||
resizeDelay?: number | ||
/** | ||
* A function that determines whether the menu should be shown or not. | ||
* If this function returns `false`, the menu will be hidden, otherwise it will be shown. | ||
*/ | ||
shouldShow?: | ||
shouldShow: | ||
| ((props: { | ||
@@ -58,2 +68,18 @@ editor: Editor | ||
| null | ||
/** | ||
* FloatingUI options. | ||
*/ | ||
options?: { | ||
strategy?: Strategy | ||
placement?: Placement | ||
offset?: OffsetOptions | boolean | ||
flip?: FlipOptions | boolean | ||
shift?: ShiftOptions | boolean | ||
arrow?: ArrowOptions | false | ||
size?: SizeOptions | boolean | ||
autoPlacement?: AutoPlacementOptions | boolean | ||
hide?: HideOptions | boolean | ||
inline?: InlineOptions | boolean | ||
} | ||
} | ||
@@ -74,10 +100,34 @@ | ||
public tippy: Instance | undefined | ||
public updateDelay: number | ||
public tippyOptions?: Partial<Props> | ||
public resizeDelay: number | ||
public updateDelay: number | ||
private updateDebounceTimer: number | undefined | ||
private resizeDebounceTimer: number | undefined | ||
private floatingUIOptions: { | ||
strategy: Strategy | ||
placement: Placement | ||
offset: OffsetOptions | boolean | ||
flip: FlipOptions | boolean | ||
shift: ShiftOptions | boolean | ||
arrow: ArrowOptions | false | ||
size: SizeOptions | boolean | ||
autoPlacement: AutoPlacementOptions | boolean | ||
hide: HideOptions | boolean | ||
inline: InlineOptions | boolean | ||
} = { | ||
strategy: 'absolute', | ||
placement: 'top', | ||
offset: 8, | ||
flip: {}, | ||
shift: {}, | ||
arrow: false, | ||
size: false, | ||
autoPlacement: false, | ||
hide: false, | ||
inline: false, | ||
} | ||
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ | ||
@@ -111,2 +161,40 @@ view, | ||
get middlewares() { | ||
const middlewares: Middleware[] = [] | ||
if (this.floatingUIOptions.flip) { | ||
middlewares.push(flip(typeof this.floatingUIOptions.flip !== 'boolean' ? this.floatingUIOptions.flip : undefined)) | ||
} | ||
if (this.floatingUIOptions.shift) { | ||
middlewares.push(shift(typeof this.floatingUIOptions.shift !== 'boolean' ? this.floatingUIOptions.shift : undefined)) | ||
} | ||
if (this.floatingUIOptions.offset) { | ||
middlewares.push(offset(typeof this.floatingUIOptions.offset !== 'boolean' ? this.floatingUIOptions.offset : undefined)) | ||
} | ||
if (this.floatingUIOptions.arrow) { | ||
middlewares.push(arrow(this.floatingUIOptions.arrow)) | ||
} | ||
if (this.floatingUIOptions.size) { | ||
middlewares.push(size(typeof this.floatingUIOptions.size !== 'boolean' ? this.floatingUIOptions.size : undefined)) | ||
} | ||
if (this.floatingUIOptions.autoPlacement) { | ||
middlewares.push(autoPlacement(typeof this.floatingUIOptions.autoPlacement !== 'boolean' ? this.floatingUIOptions.autoPlacement : undefined)) | ||
} | ||
if (this.floatingUIOptions.hide) { | ||
middlewares.push(hide(typeof this.floatingUIOptions.hide !== 'boolean' ? this.floatingUIOptions.hide : undefined)) | ||
} | ||
if (this.floatingUIOptions.inline) { | ||
middlewares.push(inline(typeof this.floatingUIOptions.inline !== 'boolean' ? this.floatingUIOptions.inline : undefined)) | ||
} | ||
return middlewares | ||
} | ||
constructor({ | ||
@@ -116,5 +204,6 @@ editor, | ||
view, | ||
tippyOptions = {}, | ||
updateDelay = 250, | ||
resizeDelay = 60, | ||
shouldShow, | ||
options, | ||
}: BubbleMenuViewProps) { | ||
@@ -125,3 +214,9 @@ this.editor = editor | ||
this.updateDelay = updateDelay | ||
this.resizeDelay = resizeDelay | ||
this.floatingUIOptions = { | ||
...this.floatingUIOptions, | ||
...options, | ||
} | ||
if (shouldShow) { | ||
@@ -135,6 +230,17 @@ this.shouldShow = shouldShow | ||
this.editor.on('blur', this.blurHandler) | ||
this.tippyOptions = tippyOptions | ||
// Detaches menu content from its current parent | ||
this.element.remove() | ||
this.element.style.visibility = 'visible' | ||
window.addEventListener('resize', () => { | ||
if (this.resizeDebounceTimer) { | ||
clearTimeout(this.resizeDebounceTimer) | ||
} | ||
this.resizeDebounceTimer = window.setTimeout(() => { | ||
this.updatePosition() | ||
}, this.resizeDelay) | ||
}) | ||
this.update(view, view.state) | ||
if (this.getShouldShow()) { | ||
this.show() | ||
} | ||
} | ||
@@ -169,29 +275,15 @@ | ||
tippyBlurHandler = (event: FocusEvent) => { | ||
this.blurHandler({ event }) | ||
} | ||
updatePosition() { | ||
const { selection } = this.editor.state | ||
createTooltip() { | ||
const { element: editorElement } = this.editor.options | ||
const editorIsAttached = !!editorElement.parentElement | ||
if (this.tippy || !editorIsAttached) { | ||
return | ||
const virtualElement = { | ||
getBoundingClientRect: () => posToDOMRect(this.view, selection.from, selection.to), | ||
} | ||
this.tippy = tippy(editorElement, { | ||
duration: 0, | ||
getReferenceClientRect: null, | ||
content: this.element, | ||
interactive: true, | ||
trigger: 'manual', | ||
placement: 'top', | ||
hideOnClick: 'toggle', | ||
...this.tippyOptions, | ||
computePosition(virtualElement, this.element, { placement: this.floatingUIOptions.placement, strategy: this.floatingUIOptions.strategy, middleware: this.middlewares }).then(({ x, y, strategy }) => { | ||
this.element.style.width = 'max-content' | ||
this.element.style.position = strategy | ||
this.element.style.left = `${x}px` | ||
this.element.style.top = `${y}px` | ||
}) | ||
// maybe we have to hide tippy on its own blur event as well | ||
if (this.tippy.popper.firstChild) { | ||
(this.tippy.popper.firstChild as HTMLElement).addEventListener('blur', this.tippyBlurHandler) | ||
} | ||
} | ||
@@ -231,15 +323,6 @@ | ||
updateHandler = (view: EditorView, selectionChanged: boolean, docChanged: boolean, oldState?: EditorState) => { | ||
const { state, composing } = view | ||
getShouldShow(oldState?: EditorState) { | ||
const { state } = this.view | ||
const { selection } = state | ||
const isSame = !selectionChanged && !docChanged | ||
if (composing || isSame) { | ||
return | ||
} | ||
this.createTooltip() | ||
// support for CellSelections | ||
const { ranges } = selection | ||
@@ -251,3 +334,3 @@ const from = Math.min(...ranges.map(range => range.$from.pos)) | ||
editor: this.editor, | ||
view, | ||
view: this.view, | ||
state, | ||
@@ -259,30 +342,23 @@ oldState, | ||
if (!shouldShow) { | ||
this.hide() | ||
return shouldShow | ||
} | ||
updateHandler = (view: EditorView, selectionChanged: boolean, docChanged: boolean, oldState?: EditorState) => { | ||
const { composing } = view | ||
const isSame = !selectionChanged && !docChanged | ||
if (composing || isSame) { | ||
return | ||
} | ||
this.tippy?.setProps({ | ||
getReferenceClientRect: | ||
this.tippyOptions?.getReferenceClientRect | ||
|| (() => { | ||
if (isNodeSelection(state.selection)) { | ||
let node = view.nodeDOM(from) as HTMLElement | ||
const shouldShow = this.getShouldShow(oldState) | ||
const nodeViewWrapper = node.dataset.nodeViewWrapper ? node : node.querySelector('[data-node-view-wrapper]') | ||
if (!shouldShow) { | ||
this.hide() | ||
if (nodeViewWrapper) { | ||
node = nodeViewWrapper.firstChild as HTMLElement | ||
} | ||
return | ||
} | ||
if (node) { | ||
return node.getBoundingClientRect() | ||
} | ||
} | ||
return posToDOMRect(view, from, to) | ||
}), | ||
}) | ||
this.updatePosition() | ||
this.show() | ||
@@ -292,17 +368,17 @@ } | ||
show() { | ||
this.tippy?.show() | ||
this.element.style.visibility = 'visible' | ||
this.element.style.opacity = '1' | ||
// attach from body | ||
document.body.appendChild(this.element) | ||
} | ||
hide() { | ||
this.tippy?.hide() | ||
this.element.style.visibility = 'hidden' | ||
this.element.style.opacity = '0' | ||
// remove from body | ||
this.element.remove() | ||
} | ||
destroy() { | ||
if (this.tippy?.popper.firstChild) { | ||
(this.tippy.popper.firstChild as HTMLElement).removeEventListener( | ||
'blur', | ||
this.tippyBlurHandler, | ||
) | ||
} | ||
this.tippy?.destroy() | ||
this.hide() | ||
this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true }) | ||
@@ -309,0 +385,0 @@ this.view.dom.removeEventListener('dragstart', this.dragstartHandler) |
@@ -24,3 +24,2 @@ import { Extension } from '@tiptap/core' | ||
element: null, | ||
tippyOptions: {}, | ||
pluginKey: 'bubbleMenu', | ||
@@ -42,3 +41,2 @@ updateDelay: undefined, | ||
element: this.options.element, | ||
tippyOptions: this.options.tippyOptions, | ||
updateDelay: this.options.updateDelay, | ||
@@ -45,0 +43,0 @@ shouldShow: this.options.shouldShow, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
252030
4933
3
1
+ Added@floating-ui/core@1.6.8(transitive)
+ Added@floating-ui/dom@1.6.12(transitive)
+ Added@floating-ui/utils@0.2.8(transitive)
+ Added@remirror/core-constants@2.0.2(transitive)
+ Added@tiptap/core@3.0.0(transitive)
+ Added@tiptap/pm@3.0.0(transitive)
+ Addedprosemirror-trailing-node@2.0.9(transitive)
- Removedtippy.js@^6.3.7
- Removed@popperjs/core@2.11.8(transitive)
- Removed@remirror/core-constants@3.0.0(transitive)
- Removed@tiptap/core@2.9.1(transitive)
- Removed@tiptap/pm@2.9.1(transitive)
- Removedprosemirror-trailing-node@3.0.0(transitive)
- Removedtippy.js@6.3.7(transitive)