@tiptap/extensions 3.24.0 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +21 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +21 -6
- package/dist/index.js.map +1 -1
- package/dist/placeholder/index.cjs +21 -6
- package/dist/placeholder/index.cjs.map +1 -1
- package/dist/placeholder/index.d.cts +8 -1
- package/dist/placeholder/index.d.ts +8 -1
- package/dist/placeholder/index.js +21 -6
- package/dist/placeholder/index.js.map +1 -1
- package/package.json +5 -5
- package/src/placeholder/types.ts +10 -1
- package/src/placeholder/utils/buildPlaceholderDecorations.ts +25 -7
- package/src/placeholder/utils/createPlaceholderDecoration.ts +1 -1
package/dist/index.cjs
CHANGED
|
@@ -284,6 +284,9 @@ function createPlaceholderDecoration(options) {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
287
|
+
function resolveEmptyNodeClass(emptyNodeClass, props) {
|
|
288
|
+
return typeof emptyNodeClass === "function" ? emptyNodeClass(props) : emptyNodeClass;
|
|
289
|
+
}
|
|
287
290
|
function buildPlaceholderDecorations({
|
|
288
291
|
editor,
|
|
289
292
|
options,
|
|
@@ -299,10 +302,6 @@ function buildPlaceholderDecorations({
|
|
|
299
302
|
const { anchor } = selection;
|
|
300
303
|
const decorations = [];
|
|
301
304
|
const isEmptyDoc = editor.isEmpty;
|
|
302
|
-
const classes = {
|
|
303
|
-
emptyEditor: options.emptyEditorClass,
|
|
304
|
-
emptyNode: options.emptyNodeClass
|
|
305
|
-
};
|
|
306
305
|
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
|
|
307
306
|
if (useResolvedPath) {
|
|
308
307
|
const resolved = doc.resolve(anchor);
|
|
@@ -317,7 +316,15 @@ function buildPlaceholderDecorations({
|
|
|
317
316
|
dataAttribute,
|
|
318
317
|
hasAnchor,
|
|
319
318
|
placeholder: options.placeholder,
|
|
320
|
-
classes
|
|
319
|
+
classes: {
|
|
320
|
+
emptyEditor: options.emptyEditorClass,
|
|
321
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
322
|
+
editor,
|
|
323
|
+
node,
|
|
324
|
+
pos: nodeStart,
|
|
325
|
+
hasAnchor
|
|
326
|
+
})
|
|
327
|
+
},
|
|
321
328
|
node,
|
|
322
329
|
pos: nodeStart
|
|
323
330
|
})
|
|
@@ -341,7 +348,15 @@ function buildPlaceholderDecorations({
|
|
|
341
348
|
dataAttribute,
|
|
342
349
|
hasAnchor,
|
|
343
350
|
placeholder: options.placeholder,
|
|
344
|
-
classes
|
|
351
|
+
classes: {
|
|
352
|
+
emptyEditor: options.emptyEditorClass,
|
|
353
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
354
|
+
editor,
|
|
355
|
+
node,
|
|
356
|
+
pos,
|
|
357
|
+
hasAnchor
|
|
358
|
+
})
|
|
359
|
+
},
|
|
345
360
|
node,
|
|
346
361
|
pos
|
|
347
362
|
})
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/character-count/character-count.ts","../src/drop-cursor/drop-cursor.ts","../src/focus/focus.ts","../src/gap-cursor/gap-cursor.ts","../src/placeholder/constants.ts","../src/placeholder/placeholder.ts","../src/placeholder/plugins/PlaceholderPlugin.ts","../src/placeholder/utils/buildPlaceholderDecorations.ts","../src/placeholder/utils/createPlaceholderDecoration.ts","../src/placeholder/utils/preparePlaceholderAttribute.ts","../src/placeholder/utils/findScrollParent.ts","../src/placeholder/utils/getViewportBoundaryPositions.ts","../src/placeholder/utils/viewportTracking.ts","../src/selection/selection.ts","../src/trailing-node/trailing-node.ts","../src/undo-redo/undo-redo.ts"],"sourcesContent":["export * from './character-count/index.js'\nexport * from './drop-cursor/index.js'\nexport * from './focus/index.js'\nexport * from './gap-cursor/index.js'\nexport * from './placeholder/index.js'\nexport * from './selection/index.js'\nexport * from './trailing-node/index.js'\nexport * from './undo-redo/index.js'\n","import { Extension } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport interface CharacterCountOptions {\n /**\n * The maximum number of characters that should be allowed. Defaults to `0`.\n * @default null\n * @example 180\n */\n limit: number | null | undefined\n /**\n * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n * If set to `nodeSize`, the nodeSize of the document is used.\n * @default 'textSize'\n * @example 'textSize'\n */\n mode: 'textSize' | 'nodeSize'\n /**\n * Sets whether the content will be automatically trimmed when programatically setting content over the limit.\n * If set to false, the user will be able to trim the text manually.\n * @default true\n * @example false\n */\n autoTrim?: boolean\n /**\n * The text counter function to use. Defaults to a simple character count.\n * @default (text) => text.length\n * @example (text) => [...new Intl.Segmenter().segment(text)].length\n */\n textCounter: (text: string) => number\n /**\n * The word counter function to use. Defaults to a simple word count.\n * @default (text) => text.split(' ').filter(word => word !== '').length\n * @example (text) => text.split(/\\s+/).filter(word => word !== '').length\n */\n wordCounter: (text: string) => number\n}\n\nexport interface CharacterCountStorage {\n /**\n * Get the number of characters for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the characters from. Defaults to the current document.\n * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n */\n characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number\n\n /**\n * Get the number of words for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the words from. Defaults to the current document.\n */\n words: (options?: { node?: ProseMirrorNode }) => number\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n characterCount: CharacterCountStorage\n }\n}\n\n/**\n * This extension allows you to count the characters and words of your document.\n * @see https://tiptap.dev/api/extensions/character-count\n */\nexport const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({\n name: 'characterCount',\n\n addOptions() {\n return {\n limit: null,\n autoTrim: true,\n mode: 'textSize',\n textCounter: text => text.length,\n wordCounter: text => text.split(' ').filter(word => word !== '').length,\n }\n },\n\n addStorage() {\n return {\n characters: () => 0,\n words: () => 0,\n }\n },\n\n onBeforeCreate() {\n this.storage.characters = options => {\n const node = options?.node || this.editor.state.doc\n const mode = options?.mode || this.options.mode\n\n if (mode === 'textSize') {\n const text = node.textBetween(0, node.content.size, undefined, ' ')\n\n return this.options.textCounter(text)\n }\n\n return node.nodeSize\n }\n\n this.storage.words = options => {\n const node = options?.node || this.editor.state.doc\n const text = node.textBetween(0, node.content.size, ' ', ' ')\n\n return this.options.wordCounter(text)\n }\n },\n\n addProseMirrorPlugins() {\n let initialEvaluationDone = false\n\n return [\n new Plugin({\n key: new PluginKey('characterCount'),\n appendTransaction: (transactions, oldState, newState) => {\n if (initialEvaluationDone) {\n return\n }\n\n const limit = this.options.limit\n const autoTrim = this.options.autoTrim\n\n if (limit === null || limit === undefined || limit === 0 || autoTrim === false) {\n initialEvaluationDone = true\n return\n }\n\n const initialContentSize = this.storage.characters({ node: newState.doc })\n\n if (initialContentSize > limit) {\n const over = initialContentSize - limit\n const from = 0\n const to = over\n\n console.warn(\n `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,\n )\n const tr = newState.tr.deleteRange(from, to)\n initialEvaluationDone = true\n return tr\n }\n\n initialEvaluationDone = true\n },\n filterTransaction: (transaction, state) => {\n const limit = this.options.limit\n\n // Nothing has changed or no limit is defined. Ignore it.\n if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {\n return true\n }\n\n const oldSize = this.storage.characters({ node: state.doc })\n const newSize = this.storage.characters({ node: transaction.doc })\n\n // Everything is in the limit. Good.\n if (newSize <= limit) {\n return true\n }\n\n // The limit has already been exceeded but will be reduced.\n if (oldSize > limit && newSize > limit && newSize <= oldSize) {\n return true\n }\n\n // The limit has already been exceeded and will be increased further.\n if (oldSize > limit && newSize > limit && newSize > oldSize) {\n return false\n }\n\n const isPaste = transaction.getMeta('paste')\n\n // Block all exceeding transactions that were not pasted.\n if (!isPaste) {\n return false\n }\n\n // For pasted content, we try to remove the exceeding content.\n const pos = transaction.selection.$head.pos\n const over = newSize - limit\n const from = pos - over\n const to = pos\n\n // It’s probably a bad idea to mutate transactions within `filterTransaction`\n // but for now this is working fine.\n transaction.deleteRange(from, to)\n\n // In some situations, the limit will continue to be exceeded after trimming.\n // This happens e.g. when truncating within a complex node (e.g. table)\n // and ProseMirror has to close this node again.\n // If this is the case, we prevent the transaction completely.\n const updatedSize = this.storage.characters({ node: transaction.doc })\n\n if (updatedSize > limit) {\n return false\n }\n\n return true\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { dropCursor } from '@tiptap/pm/dropcursor'\n\nexport interface DropcursorOptions {\n /**\n * The color of the drop cursor. Use `false` to apply no color and rely only on class.\n * @default 'currentColor'\n * @example 'red'\n */\n color?: string | false\n\n /**\n * The width of the drop cursor\n * @default 1\n * @example 2\n */\n width: number | undefined\n\n /**\n * The class of the drop cursor\n * @default undefined\n * @example 'drop-cursor'\n */\n class: string | undefined\n}\n\n/**\n * This extension allows you to add a drop cursor to your editor.\n * A drop cursor is a line that appears when you drag and drop content\n * in-between nodes.\n * @see https://tiptap.dev/api/extensions/dropcursor\n */\nexport const Dropcursor = Extension.create<DropcursorOptions>({\n name: 'dropCursor',\n\n addOptions() {\n return {\n color: 'currentColor',\n width: 1,\n class: undefined,\n }\n },\n\n addProseMirrorPlugins() {\n return [dropCursor(this.options)]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport interface FocusOptions {\n /**\n * The class name that should be added to the focused node.\n * @default 'has-focus'\n * @example 'is-focused'\n */\n className: string\n\n /**\n * The mode by which the focused node is determined.\n * - All: All nodes are marked as focused.\n * - Deepest: Only the deepest node is marked as focused.\n * - Shallowest: Only the shallowest node is marked as focused.\n *\n * @default 'all'\n * @example 'deepest'\n * @example 'shallowest'\n */\n mode: 'all' | 'deepest' | 'shallowest'\n}\n\n/**\n * This extension allows you to add a class to the focused node.\n * @see https://www.tiptap.dev/api/extensions/focus\n */\nexport const Focus = Extension.create<FocusOptions>({\n name: 'focus',\n\n addOptions() {\n return {\n className: 'has-focus',\n mode: 'all',\n }\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('focus'),\n props: {\n decorations: ({ doc, selection }) => {\n const { isEditable, isFocused } = this.editor\n const { anchor } = selection\n const decorations: Decoration[] = []\n\n if (!isEditable || !isFocused) {\n return DecorationSet.create(doc, [])\n }\n\n // Maximum Levels\n let maxLevels = 0\n\n if (this.options.mode === 'deepest') {\n doc.descendants((node, pos) => {\n if (node.isText) {\n return\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n maxLevels += 1\n })\n }\n\n // Loop through current\n let currentLevel = 0\n\n doc.descendants((node, pos) => {\n if (node.isText) {\n return false\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n currentLevel += 1\n\n const outOfScope =\n (this.options.mode === 'deepest' && maxLevels - currentLevel > 0) ||\n (this.options.mode === 'shallowest' && currentLevel > 1)\n\n if (outOfScope) {\n return this.options.mode === 'deepest'\n }\n\n decorations.push(\n Decoration.node(pos, pos + node.nodeSize, {\n class: this.options.className,\n }),\n )\n })\n\n return DecorationSet.create(doc, decorations)\n },\n },\n }),\n ]\n },\n})\n","import type { ParentConfig } from '@tiptap/core'\nimport { callOrReturn, Extension, getExtensionField } from '@tiptap/core'\nimport { gapCursor } from '@tiptap/pm/gapcursor'\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /**\n * A function to determine whether the gap cursor is allowed at the current position. Must return `true` or `false`.\n * @default null\n */\n allowGapCursor?:\n | boolean\n | null\n | ((this: {\n name: string\n options: Options\n storage: Storage\n parent: ParentConfig<NodeConfig<Options>>['allowGapCursor']\n }) => boolean | null)\n }\n}\n\n/**\n * This extension allows you to add a gap cursor to your editor.\n * A gap cursor is a cursor that appears when you click on a place\n * where no content is present, for example inbetween nodes.\n * @see https://tiptap.dev/api/extensions/gapcursor\n */\nexport const Gapcursor = Extension.create({\n name: 'gapCursor',\n\n addProseMirrorPlugins() {\n return [gapCursor()]\n },\n\n extendNodeSchema(extension) {\n const context = {\n name: extension.name,\n options: extension.options,\n storage: extension.storage,\n }\n\n return {\n allowGapCursor: callOrReturn(getExtensionField(extension, 'allowGapCursor', context)) ?? null,\n }\n },\n})\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const classes = {\n emptyEditor: options.emptyEditorClass,\n emptyNode: options.emptyNodeClass,\n }\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: PlaceholderOptions['emptyNodeClass']\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n","import { Extension, isNodeSelection } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport type SelectionOptions = {\n /**\n * The class name that should be added to the selected text.\n * @default 'selection'\n * @example 'is-selected'\n */\n className: string\n}\n\n/**\n * This extension allows you to add a class to the selected text.\n * @see https://www.tiptap.dev/api/extensions/selection\n */\nexport const Selection = Extension.create<SelectionOptions>({\n name: 'selection',\n\n addOptions() {\n return {\n className: 'selection',\n }\n },\n\n addProseMirrorPlugins() {\n const { editor, options } = this\n\n return [\n new Plugin({\n key: new PluginKey('selection'),\n props: {\n decorations(state) {\n if (\n state.selection.empty ||\n editor.isFocused ||\n !editor.isEditable ||\n isNodeSelection(state.selection) ||\n editor.view.dragging\n ) {\n return null\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(state.selection.from, state.selection.to, {\n class: options.className,\n }),\n ])\n },\n },\n }),\n ]\n },\n})\n\nexport default Selection\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { history, redo, undo } from '@tiptap/pm/history'\n\nexport interface UndoRedoOptions {\n /**\n * The amount of history events that are collected before the oldest events are discarded.\n * @default 100\n * @example 50\n */\n depth: number\n\n /**\n * The delay (in milliseconds) between changes after which a new group should be started.\n * @default 500\n * @example 1000\n */\n newGroupDelay: number\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n undoRedo: {\n /**\n * Undo recent changes\n * @example editor.commands.undo()\n */\n undo: () => ReturnType\n /**\n * Reapply reverted changes\n * @example editor.commands.redo()\n */\n redo: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to undo and redo recent changes.\n * @see https://www.tiptap.dev/api/extensions/undo-redo\n *\n * **Important**: If the `@tiptap/extension-collaboration` package is used, make sure to remove\n * the `undo-redo` extension, as it is not compatible with the `collaboration` extension.\n *\n * `@tiptap/extension-collaboration` uses its own history implementation.\n */\nexport const UndoRedo = Extension.create<UndoRedoOptions>({\n name: 'undoRedo',\n\n addOptions() {\n return {\n depth: 100,\n newGroupDelay: 500,\n }\n },\n\n addCommands() {\n return {\n undo:\n () =>\n ({ state, dispatch }) => {\n return undo(state, dispatch)\n },\n redo:\n () =>\n ({ state, dispatch }) => {\n return redo(state, dispatch)\n },\n }\n },\n\n addProseMirrorPlugins() {\n return [history(this.options)]\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-z': () => this.editor.commands.undo(),\n 'Shift-Mod-z': () => this.editor.commands.redo(),\n 'Mod-y': () => this.editor.commands.redo(),\n\n // Russian keyboard layouts\n 'Mod-я': () => this.editor.commands.undo(),\n 'Shift-Mod-я': () => this.editor.commands.redo(),\n }\n },\n})\n"],"mappings":"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","names":["import_core","import_core","import_state","import_core","import_state","import_core","import_state","import_core","import_view","import_view","import_core","import_state","import_view","import_core","import_state","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/character-count/character-count.ts","../src/drop-cursor/drop-cursor.ts","../src/focus/focus.ts","../src/gap-cursor/gap-cursor.ts","../src/placeholder/constants.ts","../src/placeholder/placeholder.ts","../src/placeholder/plugins/PlaceholderPlugin.ts","../src/placeholder/utils/buildPlaceholderDecorations.ts","../src/placeholder/utils/createPlaceholderDecoration.ts","../src/placeholder/utils/preparePlaceholderAttribute.ts","../src/placeholder/utils/findScrollParent.ts","../src/placeholder/utils/getViewportBoundaryPositions.ts","../src/placeholder/utils/viewportTracking.ts","../src/selection/selection.ts","../src/trailing-node/trailing-node.ts","../src/undo-redo/undo-redo.ts"],"sourcesContent":["export * from './character-count/index.js'\nexport * from './drop-cursor/index.js'\nexport * from './focus/index.js'\nexport * from './gap-cursor/index.js'\nexport * from './placeholder/index.js'\nexport * from './selection/index.js'\nexport * from './trailing-node/index.js'\nexport * from './undo-redo/index.js'\n","import { Extension } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport interface CharacterCountOptions {\n /**\n * The maximum number of characters that should be allowed. Defaults to `0`.\n * @default null\n * @example 180\n */\n limit: number | null | undefined\n /**\n * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n * If set to `nodeSize`, the nodeSize of the document is used.\n * @default 'textSize'\n * @example 'textSize'\n */\n mode: 'textSize' | 'nodeSize'\n /**\n * Sets whether the content will be automatically trimmed when programatically setting content over the limit.\n * If set to false, the user will be able to trim the text manually.\n * @default true\n * @example false\n */\n autoTrim?: boolean\n /**\n * The text counter function to use. Defaults to a simple character count.\n * @default (text) => text.length\n * @example (text) => [...new Intl.Segmenter().segment(text)].length\n */\n textCounter: (text: string) => number\n /**\n * The word counter function to use. Defaults to a simple word count.\n * @default (text) => text.split(' ').filter(word => word !== '').length\n * @example (text) => text.split(/\\s+/).filter(word => word !== '').length\n */\n wordCounter: (text: string) => number\n}\n\nexport interface CharacterCountStorage {\n /**\n * Get the number of characters for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the characters from. Defaults to the current document.\n * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n */\n characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number\n\n /**\n * Get the number of words for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the words from. Defaults to the current document.\n */\n words: (options?: { node?: ProseMirrorNode }) => number\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n characterCount: CharacterCountStorage\n }\n}\n\n/**\n * This extension allows you to count the characters and words of your document.\n * @see https://tiptap.dev/api/extensions/character-count\n */\nexport const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({\n name: 'characterCount',\n\n addOptions() {\n return {\n limit: null,\n autoTrim: true,\n mode: 'textSize',\n textCounter: text => text.length,\n wordCounter: text => text.split(' ').filter(word => word !== '').length,\n }\n },\n\n addStorage() {\n return {\n characters: () => 0,\n words: () => 0,\n }\n },\n\n onBeforeCreate() {\n this.storage.characters = options => {\n const node = options?.node || this.editor.state.doc\n const mode = options?.mode || this.options.mode\n\n if (mode === 'textSize') {\n const text = node.textBetween(0, node.content.size, undefined, ' ')\n\n return this.options.textCounter(text)\n }\n\n return node.nodeSize\n }\n\n this.storage.words = options => {\n const node = options?.node || this.editor.state.doc\n const text = node.textBetween(0, node.content.size, ' ', ' ')\n\n return this.options.wordCounter(text)\n }\n },\n\n addProseMirrorPlugins() {\n let initialEvaluationDone = false\n\n return [\n new Plugin({\n key: new PluginKey('characterCount'),\n appendTransaction: (transactions, oldState, newState) => {\n if (initialEvaluationDone) {\n return\n }\n\n const limit = this.options.limit\n const autoTrim = this.options.autoTrim\n\n if (limit === null || limit === undefined || limit === 0 || autoTrim === false) {\n initialEvaluationDone = true\n return\n }\n\n const initialContentSize = this.storage.characters({ node: newState.doc })\n\n if (initialContentSize > limit) {\n const over = initialContentSize - limit\n const from = 0\n const to = over\n\n console.warn(\n `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,\n )\n const tr = newState.tr.deleteRange(from, to)\n initialEvaluationDone = true\n return tr\n }\n\n initialEvaluationDone = true\n },\n filterTransaction: (transaction, state) => {\n const limit = this.options.limit\n\n // Nothing has changed or no limit is defined. Ignore it.\n if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {\n return true\n }\n\n const oldSize = this.storage.characters({ node: state.doc })\n const newSize = this.storage.characters({ node: transaction.doc })\n\n // Everything is in the limit. Good.\n if (newSize <= limit) {\n return true\n }\n\n // The limit has already been exceeded but will be reduced.\n if (oldSize > limit && newSize > limit && newSize <= oldSize) {\n return true\n }\n\n // The limit has already been exceeded and will be increased further.\n if (oldSize > limit && newSize > limit && newSize > oldSize) {\n return false\n }\n\n const isPaste = transaction.getMeta('paste')\n\n // Block all exceeding transactions that were not pasted.\n if (!isPaste) {\n return false\n }\n\n // For pasted content, we try to remove the exceeding content.\n const pos = transaction.selection.$head.pos\n const over = newSize - limit\n const from = pos - over\n const to = pos\n\n // It’s probably a bad idea to mutate transactions within `filterTransaction`\n // but for now this is working fine.\n transaction.deleteRange(from, to)\n\n // In some situations, the limit will continue to be exceeded after trimming.\n // This happens e.g. when truncating within a complex node (e.g. table)\n // and ProseMirror has to close this node again.\n // If this is the case, we prevent the transaction completely.\n const updatedSize = this.storage.characters({ node: transaction.doc })\n\n if (updatedSize > limit) {\n return false\n }\n\n return true\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { dropCursor } from '@tiptap/pm/dropcursor'\n\nexport interface DropcursorOptions {\n /**\n * The color of the drop cursor. Use `false` to apply no color and rely only on class.\n * @default 'currentColor'\n * @example 'red'\n */\n color?: string | false\n\n /**\n * The width of the drop cursor\n * @default 1\n * @example 2\n */\n width: number | undefined\n\n /**\n * The class of the drop cursor\n * @default undefined\n * @example 'drop-cursor'\n */\n class: string | undefined\n}\n\n/**\n * This extension allows you to add a drop cursor to your editor.\n * A drop cursor is a line that appears when you drag and drop content\n * in-between nodes.\n * @see https://tiptap.dev/api/extensions/dropcursor\n */\nexport const Dropcursor = Extension.create<DropcursorOptions>({\n name: 'dropCursor',\n\n addOptions() {\n return {\n color: 'currentColor',\n width: 1,\n class: undefined,\n }\n },\n\n addProseMirrorPlugins() {\n return [dropCursor(this.options)]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport interface FocusOptions {\n /**\n * The class name that should be added to the focused node.\n * @default 'has-focus'\n * @example 'is-focused'\n */\n className: string\n\n /**\n * The mode by which the focused node is determined.\n * - All: All nodes are marked as focused.\n * - Deepest: Only the deepest node is marked as focused.\n * - Shallowest: Only the shallowest node is marked as focused.\n *\n * @default 'all'\n * @example 'deepest'\n * @example 'shallowest'\n */\n mode: 'all' | 'deepest' | 'shallowest'\n}\n\n/**\n * This extension allows you to add a class to the focused node.\n * @see https://www.tiptap.dev/api/extensions/focus\n */\nexport const Focus = Extension.create<FocusOptions>({\n name: 'focus',\n\n addOptions() {\n return {\n className: 'has-focus',\n mode: 'all',\n }\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('focus'),\n props: {\n decorations: ({ doc, selection }) => {\n const { isEditable, isFocused } = this.editor\n const { anchor } = selection\n const decorations: Decoration[] = []\n\n if (!isEditable || !isFocused) {\n return DecorationSet.create(doc, [])\n }\n\n // Maximum Levels\n let maxLevels = 0\n\n if (this.options.mode === 'deepest') {\n doc.descendants((node, pos) => {\n if (node.isText) {\n return\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n maxLevels += 1\n })\n }\n\n // Loop through current\n let currentLevel = 0\n\n doc.descendants((node, pos) => {\n if (node.isText) {\n return false\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n currentLevel += 1\n\n const outOfScope =\n (this.options.mode === 'deepest' && maxLevels - currentLevel > 0) ||\n (this.options.mode === 'shallowest' && currentLevel > 1)\n\n if (outOfScope) {\n return this.options.mode === 'deepest'\n }\n\n decorations.push(\n Decoration.node(pos, pos + node.nodeSize, {\n class: this.options.className,\n }),\n )\n })\n\n return DecorationSet.create(doc, decorations)\n },\n },\n }),\n ]\n },\n})\n","import type { ParentConfig } from '@tiptap/core'\nimport { callOrReturn, Extension, getExtensionField } from '@tiptap/core'\nimport { gapCursor } from '@tiptap/pm/gapcursor'\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /**\n * A function to determine whether the gap cursor is allowed at the current position. Must return `true` or `false`.\n * @default null\n */\n allowGapCursor?:\n | boolean\n | null\n | ((this: {\n name: string\n options: Options\n storage: Storage\n parent: ParentConfig<NodeConfig<Options>>['allowGapCursor']\n }) => boolean | null)\n }\n}\n\n/**\n * This extension allows you to add a gap cursor to your editor.\n * A gap cursor is a cursor that appears when you click on a place\n * where no content is present, for example inbetween nodes.\n * @see https://tiptap.dev/api/extensions/gapcursor\n */\nexport const Gapcursor = Extension.create({\n name: 'gapCursor',\n\n addProseMirrorPlugins() {\n return [gapCursor()]\n },\n\n extendNodeSchema(extension) {\n const context = {\n name: extension.name,\n options: extension.options,\n storage: extension.storage,\n }\n\n return {\n allowGapCursor: callOrReturn(getExtensionField(extension, 'allowGapCursor', context)) ?? null,\n }\n },\n})\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\nfunction resolveEmptyNodeClass(\n emptyNodeClass: PlaceholderOptions['emptyNodeClass'],\n props: { editor: Editor; node: Node; pos: number; hasAnchor: boolean },\n): string {\n return typeof emptyNodeClass === 'function' ? emptyNodeClass(props) : emptyNodeClass\n}\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos: nodeStart,\n hasAnchor,\n }),\n },\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos,\n hasAnchor,\n }),\n },\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: string\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n","import { Extension, isNodeSelection } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport type SelectionOptions = {\n /**\n * The class name that should be added to the selected text.\n * @default 'selection'\n * @example 'is-selected'\n */\n className: string\n}\n\n/**\n * This extension allows you to add a class to the selected text.\n * @see https://www.tiptap.dev/api/extensions/selection\n */\nexport const Selection = Extension.create<SelectionOptions>({\n name: 'selection',\n\n addOptions() {\n return {\n className: 'selection',\n }\n },\n\n addProseMirrorPlugins() {\n const { editor, options } = this\n\n return [\n new Plugin({\n key: new PluginKey('selection'),\n props: {\n decorations(state) {\n if (\n state.selection.empty ||\n editor.isFocused ||\n !editor.isEditable ||\n isNodeSelection(state.selection) ||\n editor.view.dragging\n ) {\n return null\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(state.selection.from, state.selection.to, {\n class: options.className,\n }),\n ])\n },\n },\n }),\n ]\n },\n})\n\nexport default Selection\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { history, redo, undo } from '@tiptap/pm/history'\n\nexport interface UndoRedoOptions {\n /**\n * The amount of history events that are collected before the oldest events are discarded.\n * @default 100\n * @example 50\n */\n depth: number\n\n /**\n * The delay (in milliseconds) between changes after which a new group should be started.\n * @default 500\n * @example 1000\n */\n newGroupDelay: number\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n undoRedo: {\n /**\n * Undo recent changes\n * @example editor.commands.undo()\n */\n undo: () => ReturnType\n /**\n * Reapply reverted changes\n * @example editor.commands.redo()\n */\n redo: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to undo and redo recent changes.\n * @see https://www.tiptap.dev/api/extensions/undo-redo\n *\n * **Important**: If the `@tiptap/extension-collaboration` package is used, make sure to remove\n * the `undo-redo` extension, as it is not compatible with the `collaboration` extension.\n *\n * `@tiptap/extension-collaboration` uses its own history implementation.\n */\nexport const UndoRedo = Extension.create<UndoRedoOptions>({\n name: 'undoRedo',\n\n addOptions() {\n return {\n depth: 100,\n newGroupDelay: 500,\n }\n },\n\n addCommands() {\n return {\n undo:\n () =>\n ({ state, dispatch }) => {\n return undo(state, dispatch)\n },\n redo:\n () =>\n ({ state, dispatch }) => {\n return redo(state, dispatch)\n },\n }\n },\n\n addProseMirrorPlugins() {\n return [history(this.options)]\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-z': () => this.editor.commands.undo(),\n 'Shift-Mod-z': () => this.editor.commands.redo(),\n 'Mod-y': () => this.editor.commands.redo(),\n\n // Russian keyboard layouts\n 'Mod-я': () => this.editor.commands.undo(),\n 'Shift-Mod-я': () => this.editor.commands.redo(),\n }\n },\n})\n"],"mappings":"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","names":["import_core","import_core","import_state","import_core","import_state","import_core","import_state","import_core","import_view","import_view","import_core","import_state","import_view","import_core","import_state","import_core"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -159,9 +159,16 @@ interface PlaceholderOptions {
|
|
|
159
159
|
emptyEditorClass: string;
|
|
160
160
|
/**
|
|
161
161
|
* **The class name for empty nodes**
|
|
162
|
+
*
|
|
163
|
+
* You can use a function to return a dynamic class name or a string.
|
|
162
164
|
* @default 'is-empty'
|
|
163
165
|
*/
|
|
164
|
-
emptyNodeClass:
|
|
166
|
+
emptyNodeClass: ((EmptyNodeClassProps: {
|
|
167
|
+
editor: Editor;
|
|
168
|
+
node: Node;
|
|
169
|
+
pos: number;
|
|
170
|
+
hasAnchor: boolean;
|
|
171
|
+
}) => string) | string;
|
|
165
172
|
/**
|
|
166
173
|
* **The data-attribute used for the placeholder label**
|
|
167
174
|
* Will be prepended with `data-` and converted to kebab-case and cleaned of special characters.
|
package/dist/index.d.ts
CHANGED
|
@@ -159,9 +159,16 @@ interface PlaceholderOptions {
|
|
|
159
159
|
emptyEditorClass: string;
|
|
160
160
|
/**
|
|
161
161
|
* **The class name for empty nodes**
|
|
162
|
+
*
|
|
163
|
+
* You can use a function to return a dynamic class name or a string.
|
|
162
164
|
* @default 'is-empty'
|
|
163
165
|
*/
|
|
164
|
-
emptyNodeClass:
|
|
166
|
+
emptyNodeClass: ((EmptyNodeClassProps: {
|
|
167
|
+
editor: Editor;
|
|
168
|
+
node: Node;
|
|
169
|
+
pos: number;
|
|
170
|
+
hasAnchor: boolean;
|
|
171
|
+
}) => string) | string;
|
|
165
172
|
/**
|
|
166
173
|
* **The data-attribute used for the placeholder label**
|
|
167
174
|
* Will be prepended with `data-` and converted to kebab-case and cleaned of special characters.
|
package/dist/index.js
CHANGED
|
@@ -247,6 +247,9 @@ function createPlaceholderDecoration(options) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
250
|
+
function resolveEmptyNodeClass(emptyNodeClass, props) {
|
|
251
|
+
return typeof emptyNodeClass === "function" ? emptyNodeClass(props) : emptyNodeClass;
|
|
252
|
+
}
|
|
250
253
|
function buildPlaceholderDecorations({
|
|
251
254
|
editor,
|
|
252
255
|
options,
|
|
@@ -262,10 +265,6 @@ function buildPlaceholderDecorations({
|
|
|
262
265
|
const { anchor } = selection;
|
|
263
266
|
const decorations = [];
|
|
264
267
|
const isEmptyDoc = editor.isEmpty;
|
|
265
|
-
const classes = {
|
|
266
|
-
emptyEditor: options.emptyEditorClass,
|
|
267
|
-
emptyNode: options.emptyNodeClass
|
|
268
|
-
};
|
|
269
268
|
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
|
|
270
269
|
if (useResolvedPath) {
|
|
271
270
|
const resolved = doc.resolve(anchor);
|
|
@@ -280,7 +279,15 @@ function buildPlaceholderDecorations({
|
|
|
280
279
|
dataAttribute,
|
|
281
280
|
hasAnchor,
|
|
282
281
|
placeholder: options.placeholder,
|
|
283
|
-
classes
|
|
282
|
+
classes: {
|
|
283
|
+
emptyEditor: options.emptyEditorClass,
|
|
284
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
285
|
+
editor,
|
|
286
|
+
node,
|
|
287
|
+
pos: nodeStart,
|
|
288
|
+
hasAnchor
|
|
289
|
+
})
|
|
290
|
+
},
|
|
284
291
|
node,
|
|
285
292
|
pos: nodeStart
|
|
286
293
|
})
|
|
@@ -304,7 +311,15 @@ function buildPlaceholderDecorations({
|
|
|
304
311
|
dataAttribute,
|
|
305
312
|
hasAnchor,
|
|
306
313
|
placeholder: options.placeholder,
|
|
307
|
-
classes
|
|
314
|
+
classes: {
|
|
315
|
+
emptyEditor: options.emptyEditorClass,
|
|
316
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
317
|
+
editor,
|
|
318
|
+
node,
|
|
319
|
+
pos,
|
|
320
|
+
hasAnchor
|
|
321
|
+
})
|
|
322
|
+
},
|
|
308
323
|
node,
|
|
309
324
|
pos
|
|
310
325
|
})
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/character-count/character-count.ts","../src/drop-cursor/drop-cursor.ts","../src/focus/focus.ts","../src/gap-cursor/gap-cursor.ts","../src/placeholder/constants.ts","../src/placeholder/placeholder.ts","../src/placeholder/plugins/PlaceholderPlugin.ts","../src/placeholder/utils/buildPlaceholderDecorations.ts","../src/placeholder/utils/createPlaceholderDecoration.ts","../src/placeholder/utils/preparePlaceholderAttribute.ts","../src/placeholder/utils/findScrollParent.ts","../src/placeholder/utils/getViewportBoundaryPositions.ts","../src/placeholder/utils/viewportTracking.ts","../src/selection/selection.ts","../src/trailing-node/trailing-node.ts","../src/undo-redo/undo-redo.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport interface CharacterCountOptions {\n /**\n * The maximum number of characters that should be allowed. Defaults to `0`.\n * @default null\n * @example 180\n */\n limit: number | null | undefined\n /**\n * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n * If set to `nodeSize`, the nodeSize of the document is used.\n * @default 'textSize'\n * @example 'textSize'\n */\n mode: 'textSize' | 'nodeSize'\n /**\n * Sets whether the content will be automatically trimmed when programatically setting content over the limit.\n * If set to false, the user will be able to trim the text manually.\n * @default true\n * @example false\n */\n autoTrim?: boolean\n /**\n * The text counter function to use. Defaults to a simple character count.\n * @default (text) => text.length\n * @example (text) => [...new Intl.Segmenter().segment(text)].length\n */\n textCounter: (text: string) => number\n /**\n * The word counter function to use. Defaults to a simple word count.\n * @default (text) => text.split(' ').filter(word => word !== '').length\n * @example (text) => text.split(/\\s+/).filter(word => word !== '').length\n */\n wordCounter: (text: string) => number\n}\n\nexport interface CharacterCountStorage {\n /**\n * Get the number of characters for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the characters from. Defaults to the current document.\n * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n */\n characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number\n\n /**\n * Get the number of words for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the words from. Defaults to the current document.\n */\n words: (options?: { node?: ProseMirrorNode }) => number\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n characterCount: CharacterCountStorage\n }\n}\n\n/**\n * This extension allows you to count the characters and words of your document.\n * @see https://tiptap.dev/api/extensions/character-count\n */\nexport const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({\n name: 'characterCount',\n\n addOptions() {\n return {\n limit: null,\n autoTrim: true,\n mode: 'textSize',\n textCounter: text => text.length,\n wordCounter: text => text.split(' ').filter(word => word !== '').length,\n }\n },\n\n addStorage() {\n return {\n characters: () => 0,\n words: () => 0,\n }\n },\n\n onBeforeCreate() {\n this.storage.characters = options => {\n const node = options?.node || this.editor.state.doc\n const mode = options?.mode || this.options.mode\n\n if (mode === 'textSize') {\n const text = node.textBetween(0, node.content.size, undefined, ' ')\n\n return this.options.textCounter(text)\n }\n\n return node.nodeSize\n }\n\n this.storage.words = options => {\n const node = options?.node || this.editor.state.doc\n const text = node.textBetween(0, node.content.size, ' ', ' ')\n\n return this.options.wordCounter(text)\n }\n },\n\n addProseMirrorPlugins() {\n let initialEvaluationDone = false\n\n return [\n new Plugin({\n key: new PluginKey('characterCount'),\n appendTransaction: (transactions, oldState, newState) => {\n if (initialEvaluationDone) {\n return\n }\n\n const limit = this.options.limit\n const autoTrim = this.options.autoTrim\n\n if (limit === null || limit === undefined || limit === 0 || autoTrim === false) {\n initialEvaluationDone = true\n return\n }\n\n const initialContentSize = this.storage.characters({ node: newState.doc })\n\n if (initialContentSize > limit) {\n const over = initialContentSize - limit\n const from = 0\n const to = over\n\n console.warn(\n `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,\n )\n const tr = newState.tr.deleteRange(from, to)\n initialEvaluationDone = true\n return tr\n }\n\n initialEvaluationDone = true\n },\n filterTransaction: (transaction, state) => {\n const limit = this.options.limit\n\n // Nothing has changed or no limit is defined. Ignore it.\n if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {\n return true\n }\n\n const oldSize = this.storage.characters({ node: state.doc })\n const newSize = this.storage.characters({ node: transaction.doc })\n\n // Everything is in the limit. Good.\n if (newSize <= limit) {\n return true\n }\n\n // The limit has already been exceeded but will be reduced.\n if (oldSize > limit && newSize > limit && newSize <= oldSize) {\n return true\n }\n\n // The limit has already been exceeded and will be increased further.\n if (oldSize > limit && newSize > limit && newSize > oldSize) {\n return false\n }\n\n const isPaste = transaction.getMeta('paste')\n\n // Block all exceeding transactions that were not pasted.\n if (!isPaste) {\n return false\n }\n\n // For pasted content, we try to remove the exceeding content.\n const pos = transaction.selection.$head.pos\n const over = newSize - limit\n const from = pos - over\n const to = pos\n\n // It’s probably a bad idea to mutate transactions within `filterTransaction`\n // but for now this is working fine.\n transaction.deleteRange(from, to)\n\n // In some situations, the limit will continue to be exceeded after trimming.\n // This happens e.g. when truncating within a complex node (e.g. table)\n // and ProseMirror has to close this node again.\n // If this is the case, we prevent the transaction completely.\n const updatedSize = this.storage.characters({ node: transaction.doc })\n\n if (updatedSize > limit) {\n return false\n }\n\n return true\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { dropCursor } from '@tiptap/pm/dropcursor'\n\nexport interface DropcursorOptions {\n /**\n * The color of the drop cursor. Use `false` to apply no color and rely only on class.\n * @default 'currentColor'\n * @example 'red'\n */\n color?: string | false\n\n /**\n * The width of the drop cursor\n * @default 1\n * @example 2\n */\n width: number | undefined\n\n /**\n * The class of the drop cursor\n * @default undefined\n * @example 'drop-cursor'\n */\n class: string | undefined\n}\n\n/**\n * This extension allows you to add a drop cursor to your editor.\n * A drop cursor is a line that appears when you drag and drop content\n * in-between nodes.\n * @see https://tiptap.dev/api/extensions/dropcursor\n */\nexport const Dropcursor = Extension.create<DropcursorOptions>({\n name: 'dropCursor',\n\n addOptions() {\n return {\n color: 'currentColor',\n width: 1,\n class: undefined,\n }\n },\n\n addProseMirrorPlugins() {\n return [dropCursor(this.options)]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport interface FocusOptions {\n /**\n * The class name that should be added to the focused node.\n * @default 'has-focus'\n * @example 'is-focused'\n */\n className: string\n\n /**\n * The mode by which the focused node is determined.\n * - All: All nodes are marked as focused.\n * - Deepest: Only the deepest node is marked as focused.\n * - Shallowest: Only the shallowest node is marked as focused.\n *\n * @default 'all'\n * @example 'deepest'\n * @example 'shallowest'\n */\n mode: 'all' | 'deepest' | 'shallowest'\n}\n\n/**\n * This extension allows you to add a class to the focused node.\n * @see https://www.tiptap.dev/api/extensions/focus\n */\nexport const Focus = Extension.create<FocusOptions>({\n name: 'focus',\n\n addOptions() {\n return {\n className: 'has-focus',\n mode: 'all',\n }\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('focus'),\n props: {\n decorations: ({ doc, selection }) => {\n const { isEditable, isFocused } = this.editor\n const { anchor } = selection\n const decorations: Decoration[] = []\n\n if (!isEditable || !isFocused) {\n return DecorationSet.create(doc, [])\n }\n\n // Maximum Levels\n let maxLevels = 0\n\n if (this.options.mode === 'deepest') {\n doc.descendants((node, pos) => {\n if (node.isText) {\n return\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n maxLevels += 1\n })\n }\n\n // Loop through current\n let currentLevel = 0\n\n doc.descendants((node, pos) => {\n if (node.isText) {\n return false\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n currentLevel += 1\n\n const outOfScope =\n (this.options.mode === 'deepest' && maxLevels - currentLevel > 0) ||\n (this.options.mode === 'shallowest' && currentLevel > 1)\n\n if (outOfScope) {\n return this.options.mode === 'deepest'\n }\n\n decorations.push(\n Decoration.node(pos, pos + node.nodeSize, {\n class: this.options.className,\n }),\n )\n })\n\n return DecorationSet.create(doc, decorations)\n },\n },\n }),\n ]\n },\n})\n","import type { ParentConfig } from '@tiptap/core'\nimport { callOrReturn, Extension, getExtensionField } from '@tiptap/core'\nimport { gapCursor } from '@tiptap/pm/gapcursor'\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /**\n * A function to determine whether the gap cursor is allowed at the current position. Must return `true` or `false`.\n * @default null\n */\n allowGapCursor?:\n | boolean\n | null\n | ((this: {\n name: string\n options: Options\n storage: Storage\n parent: ParentConfig<NodeConfig<Options>>['allowGapCursor']\n }) => boolean | null)\n }\n}\n\n/**\n * This extension allows you to add a gap cursor to your editor.\n * A gap cursor is a cursor that appears when you click on a place\n * where no content is present, for example inbetween nodes.\n * @see https://tiptap.dev/api/extensions/gapcursor\n */\nexport const Gapcursor = Extension.create({\n name: 'gapCursor',\n\n addProseMirrorPlugins() {\n return [gapCursor()]\n },\n\n extendNodeSchema(extension) {\n const context = {\n name: extension.name,\n options: extension.options,\n storage: extension.storage,\n }\n\n return {\n allowGapCursor: callOrReturn(getExtensionField(extension, 'allowGapCursor', context)) ?? null,\n }\n },\n})\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const classes = {\n emptyEditor: options.emptyEditorClass,\n emptyNode: options.emptyNodeClass,\n }\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: PlaceholderOptions['emptyNodeClass']\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n","import { Extension, isNodeSelection } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport type SelectionOptions = {\n /**\n * The class name that should be added to the selected text.\n * @default 'selection'\n * @example 'is-selected'\n */\n className: string\n}\n\n/**\n * This extension allows you to add a class to the selected text.\n * @see https://www.tiptap.dev/api/extensions/selection\n */\nexport const Selection = Extension.create<SelectionOptions>({\n name: 'selection',\n\n addOptions() {\n return {\n className: 'selection',\n }\n },\n\n addProseMirrorPlugins() {\n const { editor, options } = this\n\n return [\n new Plugin({\n key: new PluginKey('selection'),\n props: {\n decorations(state) {\n if (\n state.selection.empty ||\n editor.isFocused ||\n !editor.isEditable ||\n isNodeSelection(state.selection) ||\n editor.view.dragging\n ) {\n return null\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(state.selection.from, state.selection.to, {\n class: options.className,\n }),\n ])\n },\n },\n }),\n ]\n },\n})\n\nexport default Selection\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { history, redo, undo } from '@tiptap/pm/history'\n\nexport interface UndoRedoOptions {\n /**\n * The amount of history events that are collected before the oldest events are discarded.\n * @default 100\n * @example 50\n */\n depth: number\n\n /**\n * The delay (in milliseconds) between changes after which a new group should be started.\n * @default 500\n * @example 1000\n */\n newGroupDelay: number\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n undoRedo: {\n /**\n * Undo recent changes\n * @example editor.commands.undo()\n */\n undo: () => ReturnType\n /**\n * Reapply reverted changes\n * @example editor.commands.redo()\n */\n redo: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to undo and redo recent changes.\n * @see https://www.tiptap.dev/api/extensions/undo-redo\n *\n * **Important**: If the `@tiptap/extension-collaboration` package is used, make sure to remove\n * the `undo-redo` extension, as it is not compatible with the `collaboration` extension.\n *\n * `@tiptap/extension-collaboration` uses its own history implementation.\n */\nexport const UndoRedo = Extension.create<UndoRedoOptions>({\n name: 'undoRedo',\n\n addOptions() {\n return {\n depth: 100,\n newGroupDelay: 500,\n }\n },\n\n addCommands() {\n return {\n undo:\n () =>\n ({ state, dispatch }) => {\n return undo(state, dispatch)\n },\n redo:\n () =>\n ({ state, dispatch }) => {\n return redo(state, dispatch)\n },\n }\n },\n\n addProseMirrorPlugins() {\n return [history(this.options)]\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-z': () => this.editor.commands.undo(),\n 'Shift-Mod-z': () => this.editor.commands.redo(),\n 'Mod-y': () => this.editor.commands.redo(),\n\n // Russian keyboard layouts\n 'Mod-я': () => this.editor.commands.undo(),\n 'Shift-Mod-я': () => this.editor.commands.redo(),\n }\n },\n})\n"],"mappings":"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","names":["Extension","Extension","Plugin","PluginKey","Extension","PluginKey","Extension","Plugin","DecorationSet","Decoration","DecorationSet","Plugin","Extension","Extension","Plugin","PluginKey","Decoration","DecorationSet","Extension","Plugin","PluginKey","Extension"]}
|
|
1
|
+
{"version":3,"sources":["../src/character-count/character-count.ts","../src/drop-cursor/drop-cursor.ts","../src/focus/focus.ts","../src/gap-cursor/gap-cursor.ts","../src/placeholder/constants.ts","../src/placeholder/placeholder.ts","../src/placeholder/plugins/PlaceholderPlugin.ts","../src/placeholder/utils/buildPlaceholderDecorations.ts","../src/placeholder/utils/createPlaceholderDecoration.ts","../src/placeholder/utils/preparePlaceholderAttribute.ts","../src/placeholder/utils/findScrollParent.ts","../src/placeholder/utils/getViewportBoundaryPositions.ts","../src/placeholder/utils/viewportTracking.ts","../src/selection/selection.ts","../src/trailing-node/trailing-node.ts","../src/undo-redo/undo-redo.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport interface CharacterCountOptions {\n /**\n * The maximum number of characters that should be allowed. Defaults to `0`.\n * @default null\n * @example 180\n */\n limit: number | null | undefined\n /**\n * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n * If set to `nodeSize`, the nodeSize of the document is used.\n * @default 'textSize'\n * @example 'textSize'\n */\n mode: 'textSize' | 'nodeSize'\n /**\n * Sets whether the content will be automatically trimmed when programatically setting content over the limit.\n * If set to false, the user will be able to trim the text manually.\n * @default true\n * @example false\n */\n autoTrim?: boolean\n /**\n * The text counter function to use. Defaults to a simple character count.\n * @default (text) => text.length\n * @example (text) => [...new Intl.Segmenter().segment(text)].length\n */\n textCounter: (text: string) => number\n /**\n * The word counter function to use. Defaults to a simple word count.\n * @default (text) => text.split(' ').filter(word => word !== '').length\n * @example (text) => text.split(/\\s+/).filter(word => word !== '').length\n */\n wordCounter: (text: string) => number\n}\n\nexport interface CharacterCountStorage {\n /**\n * Get the number of characters for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the characters from. Defaults to the current document.\n * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n */\n characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number\n\n /**\n * Get the number of words for the current document.\n * @param options The options for the character count. (optional)\n * @param options.node The node to get the words from. Defaults to the current document.\n */\n words: (options?: { node?: ProseMirrorNode }) => number\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n characterCount: CharacterCountStorage\n }\n}\n\n/**\n * This extension allows you to count the characters and words of your document.\n * @see https://tiptap.dev/api/extensions/character-count\n */\nexport const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({\n name: 'characterCount',\n\n addOptions() {\n return {\n limit: null,\n autoTrim: true,\n mode: 'textSize',\n textCounter: text => text.length,\n wordCounter: text => text.split(' ').filter(word => word !== '').length,\n }\n },\n\n addStorage() {\n return {\n characters: () => 0,\n words: () => 0,\n }\n },\n\n onBeforeCreate() {\n this.storage.characters = options => {\n const node = options?.node || this.editor.state.doc\n const mode = options?.mode || this.options.mode\n\n if (mode === 'textSize') {\n const text = node.textBetween(0, node.content.size, undefined, ' ')\n\n return this.options.textCounter(text)\n }\n\n return node.nodeSize\n }\n\n this.storage.words = options => {\n const node = options?.node || this.editor.state.doc\n const text = node.textBetween(0, node.content.size, ' ', ' ')\n\n return this.options.wordCounter(text)\n }\n },\n\n addProseMirrorPlugins() {\n let initialEvaluationDone = false\n\n return [\n new Plugin({\n key: new PluginKey('characterCount'),\n appendTransaction: (transactions, oldState, newState) => {\n if (initialEvaluationDone) {\n return\n }\n\n const limit = this.options.limit\n const autoTrim = this.options.autoTrim\n\n if (limit === null || limit === undefined || limit === 0 || autoTrim === false) {\n initialEvaluationDone = true\n return\n }\n\n const initialContentSize = this.storage.characters({ node: newState.doc })\n\n if (initialContentSize > limit) {\n const over = initialContentSize - limit\n const from = 0\n const to = over\n\n console.warn(\n `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,\n )\n const tr = newState.tr.deleteRange(from, to)\n initialEvaluationDone = true\n return tr\n }\n\n initialEvaluationDone = true\n },\n filterTransaction: (transaction, state) => {\n const limit = this.options.limit\n\n // Nothing has changed or no limit is defined. Ignore it.\n if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {\n return true\n }\n\n const oldSize = this.storage.characters({ node: state.doc })\n const newSize = this.storage.characters({ node: transaction.doc })\n\n // Everything is in the limit. Good.\n if (newSize <= limit) {\n return true\n }\n\n // The limit has already been exceeded but will be reduced.\n if (oldSize > limit && newSize > limit && newSize <= oldSize) {\n return true\n }\n\n // The limit has already been exceeded and will be increased further.\n if (oldSize > limit && newSize > limit && newSize > oldSize) {\n return false\n }\n\n const isPaste = transaction.getMeta('paste')\n\n // Block all exceeding transactions that were not pasted.\n if (!isPaste) {\n return false\n }\n\n // For pasted content, we try to remove the exceeding content.\n const pos = transaction.selection.$head.pos\n const over = newSize - limit\n const from = pos - over\n const to = pos\n\n // It’s probably a bad idea to mutate transactions within `filterTransaction`\n // but for now this is working fine.\n transaction.deleteRange(from, to)\n\n // In some situations, the limit will continue to be exceeded after trimming.\n // This happens e.g. when truncating within a complex node (e.g. table)\n // and ProseMirror has to close this node again.\n // If this is the case, we prevent the transaction completely.\n const updatedSize = this.storage.characters({ node: transaction.doc })\n\n if (updatedSize > limit) {\n return false\n }\n\n return true\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { dropCursor } from '@tiptap/pm/dropcursor'\n\nexport interface DropcursorOptions {\n /**\n * The color of the drop cursor. Use `false` to apply no color and rely only on class.\n * @default 'currentColor'\n * @example 'red'\n */\n color?: string | false\n\n /**\n * The width of the drop cursor\n * @default 1\n * @example 2\n */\n width: number | undefined\n\n /**\n * The class of the drop cursor\n * @default undefined\n * @example 'drop-cursor'\n */\n class: string | undefined\n}\n\n/**\n * This extension allows you to add a drop cursor to your editor.\n * A drop cursor is a line that appears when you drag and drop content\n * in-between nodes.\n * @see https://tiptap.dev/api/extensions/dropcursor\n */\nexport const Dropcursor = Extension.create<DropcursorOptions>({\n name: 'dropCursor',\n\n addOptions() {\n return {\n color: 'currentColor',\n width: 1,\n class: undefined,\n }\n },\n\n addProseMirrorPlugins() {\n return [dropCursor(this.options)]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport interface FocusOptions {\n /**\n * The class name that should be added to the focused node.\n * @default 'has-focus'\n * @example 'is-focused'\n */\n className: string\n\n /**\n * The mode by which the focused node is determined.\n * - All: All nodes are marked as focused.\n * - Deepest: Only the deepest node is marked as focused.\n * - Shallowest: Only the shallowest node is marked as focused.\n *\n * @default 'all'\n * @example 'deepest'\n * @example 'shallowest'\n */\n mode: 'all' | 'deepest' | 'shallowest'\n}\n\n/**\n * This extension allows you to add a class to the focused node.\n * @see https://www.tiptap.dev/api/extensions/focus\n */\nexport const Focus = Extension.create<FocusOptions>({\n name: 'focus',\n\n addOptions() {\n return {\n className: 'has-focus',\n mode: 'all',\n }\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('focus'),\n props: {\n decorations: ({ doc, selection }) => {\n const { isEditable, isFocused } = this.editor\n const { anchor } = selection\n const decorations: Decoration[] = []\n\n if (!isEditable || !isFocused) {\n return DecorationSet.create(doc, [])\n }\n\n // Maximum Levels\n let maxLevels = 0\n\n if (this.options.mode === 'deepest') {\n doc.descendants((node, pos) => {\n if (node.isText) {\n return\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n maxLevels += 1\n })\n }\n\n // Loop through current\n let currentLevel = 0\n\n doc.descendants((node, pos) => {\n if (node.isText) {\n return false\n }\n\n const isCurrent = anchor >= pos && anchor <= pos + node.nodeSize - 1\n\n if (!isCurrent) {\n return false\n }\n\n currentLevel += 1\n\n const outOfScope =\n (this.options.mode === 'deepest' && maxLevels - currentLevel > 0) ||\n (this.options.mode === 'shallowest' && currentLevel > 1)\n\n if (outOfScope) {\n return this.options.mode === 'deepest'\n }\n\n decorations.push(\n Decoration.node(pos, pos + node.nodeSize, {\n class: this.options.className,\n }),\n )\n })\n\n return DecorationSet.create(doc, decorations)\n },\n },\n }),\n ]\n },\n})\n","import type { ParentConfig } from '@tiptap/core'\nimport { callOrReturn, Extension, getExtensionField } from '@tiptap/core'\nimport { gapCursor } from '@tiptap/pm/gapcursor'\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /**\n * A function to determine whether the gap cursor is allowed at the current position. Must return `true` or `false`.\n * @default null\n */\n allowGapCursor?:\n | boolean\n | null\n | ((this: {\n name: string\n options: Options\n storage: Storage\n parent: ParentConfig<NodeConfig<Options>>['allowGapCursor']\n }) => boolean | null)\n }\n}\n\n/**\n * This extension allows you to add a gap cursor to your editor.\n * A gap cursor is a cursor that appears when you click on a place\n * where no content is present, for example inbetween nodes.\n * @see https://tiptap.dev/api/extensions/gapcursor\n */\nexport const Gapcursor = Extension.create({\n name: 'gapCursor',\n\n addProseMirrorPlugins() {\n return [gapCursor()]\n },\n\n extendNodeSchema(extension) {\n const context = {\n name: extension.name,\n options: extension.options,\n storage: extension.storage,\n }\n\n return {\n allowGapCursor: callOrReturn(getExtensionField(extension, 'allowGapCursor', context)) ?? null,\n }\n },\n})\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\nfunction resolveEmptyNodeClass(\n emptyNodeClass: PlaceholderOptions['emptyNodeClass'],\n props: { editor: Editor; node: Node; pos: number; hasAnchor: boolean },\n): string {\n return typeof emptyNodeClass === 'function' ? emptyNodeClass(props) : emptyNodeClass\n}\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos: nodeStart,\n hasAnchor,\n }),\n },\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos,\n hasAnchor,\n }),\n },\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: string\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n","import { Extension, isNodeSelection } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nexport type SelectionOptions = {\n /**\n * The class name that should be added to the selected text.\n * @default 'selection'\n * @example 'is-selected'\n */\n className: string\n}\n\n/**\n * This extension allows you to add a class to the selected text.\n * @see https://www.tiptap.dev/api/extensions/selection\n */\nexport const Selection = Extension.create<SelectionOptions>({\n name: 'selection',\n\n addOptions() {\n return {\n className: 'selection',\n }\n },\n\n addProseMirrorPlugins() {\n const { editor, options } = this\n\n return [\n new Plugin({\n key: new PluginKey('selection'),\n props: {\n decorations(state) {\n if (\n state.selection.empty ||\n editor.isFocused ||\n !editor.isEditable ||\n isNodeSelection(state.selection) ||\n editor.view.dragging\n ) {\n return null\n }\n\n return DecorationSet.create(state.doc, [\n Decoration.inline(state.selection.from, state.selection.to, {\n class: options.className,\n }),\n ])\n },\n },\n }),\n ]\n },\n})\n\nexport default Selection\n","import { Extension } from '@tiptap/core'\nimport type { Node, NodeType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport const skipTrailingNodeMeta = 'skipTrailingNode'\n\nfunction nodeEqualsType({\n types,\n node,\n}: {\n types: NodeType | NodeType[]\n node: Node | null | undefined\n}) {\n return (node && Array.isArray(types) && types.includes(node.type)) || node?.type === types\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport interface TrailingNodeOptions {\n /**\n * The node type that should be inserted at the end of the document.\n * @note the node will always be added to the `notAfter` lists to\n * prevent an infinite loop.\n * @default undefined\n */\n node?: string\n /**\n * The node types after which the trailing node should not be inserted.\n * @default ['paragraph']\n */\n notAfter?: string | string[]\n}\n\n/**\n * This extension allows you to add an extra node at the end of the document.\n * @see https://www.tiptap.dev/api/extensions/trailing-node\n */\nexport const TrailingNode = Extension.create<TrailingNodeOptions>({\n name: 'trailingNode',\n\n addOptions() {\n return {\n node: undefined,\n notAfter: [],\n }\n },\n\n addProseMirrorPlugins() {\n const plugin = new PluginKey(this.name)\n const defaultNode =\n this.options.node ||\n this.editor.schema.topNodeType.contentMatch.defaultType?.name ||\n 'paragraph'\n\n const disabledNodes = Object.entries(this.editor.schema.nodes)\n .map(([, value]) => value)\n .filter(node => (this.options.notAfter || []).concat(defaultNode).includes(node.name))\n\n return [\n new Plugin({\n key: plugin,\n appendTransaction: (transactions, __, state) => {\n const { doc, tr, schema } = state\n const shouldInsertNodeAtEnd = plugin.getState(state)\n const endPosition = doc.content.size\n const type = schema.nodes[defaultNode]\n\n if (transactions.some(transaction => transaction.getMeta(skipTrailingNodeMeta))) {\n return\n }\n\n if (!shouldInsertNodeAtEnd) {\n return\n }\n\n return tr.insert(endPosition, type.create())\n },\n state: {\n init: (_, state) => {\n const lastNode = state.tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n apply: (tr, value) => {\n if (!tr.docChanged) {\n return value\n }\n\n // Ignore transactions from UniqueID extension to prevent infinite loops\n // when UniqueID adds IDs to newly inserted trailing nodes\n if (tr.getMeta('__uniqueIDTransaction')) {\n return value\n }\n\n const lastNode = tr.doc.lastChild\n\n return !nodeEqualsType({ node: lastNode, types: disabledNodes })\n },\n },\n }),\n ]\n },\n})\n","import { Extension } from '@tiptap/core'\nimport { history, redo, undo } from '@tiptap/pm/history'\n\nexport interface UndoRedoOptions {\n /**\n * The amount of history events that are collected before the oldest events are discarded.\n * @default 100\n * @example 50\n */\n depth: number\n\n /**\n * The delay (in milliseconds) between changes after which a new group should be started.\n * @default 500\n * @example 1000\n */\n newGroupDelay: number\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n undoRedo: {\n /**\n * Undo recent changes\n * @example editor.commands.undo()\n */\n undo: () => ReturnType\n /**\n * Reapply reverted changes\n * @example editor.commands.redo()\n */\n redo: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to undo and redo recent changes.\n * @see https://www.tiptap.dev/api/extensions/undo-redo\n *\n * **Important**: If the `@tiptap/extension-collaboration` package is used, make sure to remove\n * the `undo-redo` extension, as it is not compatible with the `collaboration` extension.\n *\n * `@tiptap/extension-collaboration` uses its own history implementation.\n */\nexport const UndoRedo = Extension.create<UndoRedoOptions>({\n name: 'undoRedo',\n\n addOptions() {\n return {\n depth: 100,\n newGroupDelay: 500,\n }\n },\n\n addCommands() {\n return {\n undo:\n () =>\n ({ state, dispatch }) => {\n return undo(state, dispatch)\n },\n redo:\n () =>\n ({ state, dispatch }) => {\n return redo(state, dispatch)\n },\n }\n },\n\n addProseMirrorPlugins() {\n return [history(this.options)]\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-z': () => this.editor.commands.undo(),\n 'Shift-Mod-z': () => this.editor.commands.redo(),\n 'Mod-y': () => this.editor.commands.redo(),\n\n // Russian keyboard layouts\n 'Mod-я': () => this.editor.commands.undo(),\n 'Shift-Mod-я': () => this.editor.commands.redo(),\n }\n },\n})\n"],"mappings":"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","names":["Extension","Extension","Plugin","PluginKey","Extension","PluginKey","Extension","Plugin","DecorationSet","Decoration","DecorationSet","Plugin","Extension","Extension","Plugin","PluginKey","Decoration","DecorationSet","Extension","Plugin","PluginKey","Extension"]}
|
|
@@ -72,6 +72,9 @@ function createPlaceholderDecoration(options) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
75
|
+
function resolveEmptyNodeClass(emptyNodeClass, props) {
|
|
76
|
+
return typeof emptyNodeClass === "function" ? emptyNodeClass(props) : emptyNodeClass;
|
|
77
|
+
}
|
|
75
78
|
function buildPlaceholderDecorations({
|
|
76
79
|
editor,
|
|
77
80
|
options,
|
|
@@ -87,10 +90,6 @@ function buildPlaceholderDecorations({
|
|
|
87
90
|
const { anchor } = selection;
|
|
88
91
|
const decorations = [];
|
|
89
92
|
const isEmptyDoc = editor.isEmpty;
|
|
90
|
-
const classes = {
|
|
91
|
-
emptyEditor: options.emptyEditorClass,
|
|
92
|
-
emptyNode: options.emptyNodeClass
|
|
93
|
-
};
|
|
94
93
|
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
|
|
95
94
|
if (useResolvedPath) {
|
|
96
95
|
const resolved = doc.resolve(anchor);
|
|
@@ -105,7 +104,15 @@ function buildPlaceholderDecorations({
|
|
|
105
104
|
dataAttribute,
|
|
106
105
|
hasAnchor,
|
|
107
106
|
placeholder: options.placeholder,
|
|
108
|
-
classes
|
|
107
|
+
classes: {
|
|
108
|
+
emptyEditor: options.emptyEditorClass,
|
|
109
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
110
|
+
editor,
|
|
111
|
+
node,
|
|
112
|
+
pos: nodeStart,
|
|
113
|
+
hasAnchor
|
|
114
|
+
})
|
|
115
|
+
},
|
|
109
116
|
node,
|
|
110
117
|
pos: nodeStart
|
|
111
118
|
})
|
|
@@ -129,7 +136,15 @@ function buildPlaceholderDecorations({
|
|
|
129
136
|
dataAttribute,
|
|
130
137
|
hasAnchor,
|
|
131
138
|
placeholder: options.placeholder,
|
|
132
|
-
classes
|
|
139
|
+
classes: {
|
|
140
|
+
emptyEditor: options.emptyEditorClass,
|
|
141
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
142
|
+
editor,
|
|
143
|
+
node,
|
|
144
|
+
pos,
|
|
145
|
+
hasAnchor
|
|
146
|
+
})
|
|
147
|
+
},
|
|
133
148
|
node,
|
|
134
149
|
pos
|
|
135
150
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/placeholder/index.ts","../../src/placeholder/constants.ts","../../src/placeholder/placeholder.ts","../../src/placeholder/plugins/PlaceholderPlugin.ts","../../src/placeholder/utils/buildPlaceholderDecorations.ts","../../src/placeholder/utils/createPlaceholderDecoration.ts","../../src/placeholder/utils/preparePlaceholderAttribute.ts","../../src/placeholder/utils/findScrollParent.ts","../../src/placeholder/utils/getViewportBoundaryPositions.ts","../../src/placeholder/utils/viewportTracking.ts"],"sourcesContent":["export { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from './constants.js'\nexport * from './placeholder.js'\nexport * from './types.js'\nexport { preparePlaceholderAttribute } from './utils/preparePlaceholderAttribute.js'\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const classes = {\n emptyEditor: options.emptyEditorClass,\n emptyNode: options.emptyNodeClass,\n }\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: PlaceholderOptions['emptyNodeClass']\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n"],"mappings":"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","names":["import_core","import_state","import_view"]}
|
|
1
|
+
{"version":3,"sources":["../../src/placeholder/index.ts","../../src/placeholder/constants.ts","../../src/placeholder/placeholder.ts","../../src/placeholder/plugins/PlaceholderPlugin.ts","../../src/placeholder/utils/buildPlaceholderDecorations.ts","../../src/placeholder/utils/createPlaceholderDecoration.ts","../../src/placeholder/utils/preparePlaceholderAttribute.ts","../../src/placeholder/utils/findScrollParent.ts","../../src/placeholder/utils/getViewportBoundaryPositions.ts","../../src/placeholder/utils/viewportTracking.ts"],"sourcesContent":["export { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from './constants.js'\nexport * from './placeholder.js'\nexport * from './types.js'\nexport { preparePlaceholderAttribute } from './utils/preparePlaceholderAttribute.js'\n","import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\nfunction resolveEmptyNodeClass(\n emptyNodeClass: PlaceholderOptions['emptyNodeClass'],\n props: { editor: Editor; node: Node; pos: number; hasAnchor: boolean },\n): string {\n return typeof emptyNodeClass === 'function' ? emptyNodeClass(props) : emptyNodeClass\n}\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos: nodeStart,\n hasAnchor,\n }),\n },\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos,\n hasAnchor,\n }),\n },\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: string\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n"],"mappings":"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","names":["import_core","import_state","import_view"]}
|
|
@@ -19,9 +19,16 @@ interface PlaceholderOptions {
|
|
|
19
19
|
emptyEditorClass: string;
|
|
20
20
|
/**
|
|
21
21
|
* **The class name for empty nodes**
|
|
22
|
+
*
|
|
23
|
+
* You can use a function to return a dynamic class name or a string.
|
|
22
24
|
* @default 'is-empty'
|
|
23
25
|
*/
|
|
24
|
-
emptyNodeClass:
|
|
26
|
+
emptyNodeClass: ((EmptyNodeClassProps: {
|
|
27
|
+
editor: Editor;
|
|
28
|
+
node: Node;
|
|
29
|
+
pos: number;
|
|
30
|
+
hasAnchor: boolean;
|
|
31
|
+
}) => string) | string;
|
|
25
32
|
/**
|
|
26
33
|
* **The data-attribute used for the placeholder label**
|
|
27
34
|
* Will be prepended with `data-` and converted to kebab-case and cleaned of special characters.
|
|
@@ -19,9 +19,16 @@ interface PlaceholderOptions {
|
|
|
19
19
|
emptyEditorClass: string;
|
|
20
20
|
/**
|
|
21
21
|
* **The class name for empty nodes**
|
|
22
|
+
*
|
|
23
|
+
* You can use a function to return a dynamic class name or a string.
|
|
22
24
|
* @default 'is-empty'
|
|
23
25
|
*/
|
|
24
|
-
emptyNodeClass:
|
|
26
|
+
emptyNodeClass: ((EmptyNodeClassProps: {
|
|
27
|
+
editor: Editor;
|
|
28
|
+
node: Node;
|
|
29
|
+
pos: number;
|
|
30
|
+
hasAnchor: boolean;
|
|
31
|
+
}) => string) | string;
|
|
25
32
|
/**
|
|
26
33
|
* **The data-attribute used for the placeholder label**
|
|
27
34
|
* Will be prepended with `data-` and converted to kebab-case and cleaned of special characters.
|
|
@@ -43,6 +43,9 @@ function createPlaceholderDecoration(options) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// src/placeholder/utils/buildPlaceholderDecorations.ts
|
|
46
|
+
function resolveEmptyNodeClass(emptyNodeClass, props) {
|
|
47
|
+
return typeof emptyNodeClass === "function" ? emptyNodeClass(props) : emptyNodeClass;
|
|
48
|
+
}
|
|
46
49
|
function buildPlaceholderDecorations({
|
|
47
50
|
editor,
|
|
48
51
|
options,
|
|
@@ -58,10 +61,6 @@ function buildPlaceholderDecorations({
|
|
|
58
61
|
const { anchor } = selection;
|
|
59
62
|
const decorations = [];
|
|
60
63
|
const isEmptyDoc = editor.isEmpty;
|
|
61
|
-
const classes = {
|
|
62
|
-
emptyEditor: options.emptyEditorClass,
|
|
63
|
-
emptyNode: options.emptyNodeClass
|
|
64
|
-
};
|
|
65
64
|
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
|
|
66
65
|
if (useResolvedPath) {
|
|
67
66
|
const resolved = doc.resolve(anchor);
|
|
@@ -76,7 +75,15 @@ function buildPlaceholderDecorations({
|
|
|
76
75
|
dataAttribute,
|
|
77
76
|
hasAnchor,
|
|
78
77
|
placeholder: options.placeholder,
|
|
79
|
-
classes
|
|
78
|
+
classes: {
|
|
79
|
+
emptyEditor: options.emptyEditorClass,
|
|
80
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
81
|
+
editor,
|
|
82
|
+
node,
|
|
83
|
+
pos: nodeStart,
|
|
84
|
+
hasAnchor
|
|
85
|
+
})
|
|
86
|
+
},
|
|
80
87
|
node,
|
|
81
88
|
pos: nodeStart
|
|
82
89
|
})
|
|
@@ -100,7 +107,15 @@ function buildPlaceholderDecorations({
|
|
|
100
107
|
dataAttribute,
|
|
101
108
|
hasAnchor,
|
|
102
109
|
placeholder: options.placeholder,
|
|
103
|
-
classes
|
|
110
|
+
classes: {
|
|
111
|
+
emptyEditor: options.emptyEditorClass,
|
|
112
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
113
|
+
editor,
|
|
114
|
+
node,
|
|
115
|
+
pos,
|
|
116
|
+
hasAnchor
|
|
117
|
+
})
|
|
118
|
+
},
|
|
104
119
|
node,
|
|
105
120
|
pos
|
|
106
121
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/placeholder/constants.ts","../../src/placeholder/placeholder.ts","../../src/placeholder/plugins/PlaceholderPlugin.ts","../../src/placeholder/utils/buildPlaceholderDecorations.ts","../../src/placeholder/utils/createPlaceholderDecoration.ts","../../src/placeholder/utils/preparePlaceholderAttribute.ts","../../src/placeholder/utils/findScrollParent.ts","../../src/placeholder/utils/getViewportBoundaryPositions.ts","../../src/placeholder/utils/viewportTracking.ts"],"sourcesContent":["import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const classes = {\n emptyEditor: options.emptyEditorClass,\n emptyNode: options.emptyNodeClass,\n }\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes,\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: PlaceholderOptions['emptyNodeClass']\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/placeholder/constants.ts","../../src/placeholder/placeholder.ts","../../src/placeholder/plugins/PlaceholderPlugin.ts","../../src/placeholder/utils/buildPlaceholderDecorations.ts","../../src/placeholder/utils/createPlaceholderDecoration.ts","../../src/placeholder/utils/preparePlaceholderAttribute.ts","../../src/placeholder/utils/findScrollParent.ts","../../src/placeholder/utils/getViewportBoundaryPositions.ts","../../src/placeholder/utils/viewportTracking.ts"],"sourcesContent":["import { PluginKey } from '@tiptap/pm/state'\n\nimport type { ViewportState } from './types.js'\n\n/** The default data attribute label */\nexport const DEFAULT_DATA_ATTRIBUTE = 'placeholder'\n\n/** The plugin key used to store and read the placeholder viewport state */\nexport const PLUGIN_KEY = new PluginKey<ViewportState>('tiptap__placeholder')\n\n/**\n * Extra pixels added above and below the visible viewport when computing the\n * range of nodes to decorate. Decorating slightly beyond the fold means a\n * node already has its placeholder before it scrolls into view, which hides\n * the latency introduced by the throttled viewport recompute.\n */\nexport const VIEWPORT_OVERSCAN_PX = 200\n","import { Extension } from '@tiptap/core'\n\nimport { DEFAULT_DATA_ATTRIBUTE } from './constants.js'\nimport { createPlaceholderPlugin } from './plugins/PlaceholderPlugin.js'\nimport type { PlaceholderOptions } from './types.js'\n\n/**\n * This extension allows you to add a placeholder to your editor.\n * A placeholder is a text that appears when the editor or a node is empty.\n * @see https://www.tiptap.dev/api/extensions/placeholder\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n dataAttribute: DEFAULT_DATA_ATTRIBUTE,\n placeholder: 'Write something …',\n showOnlyWhenEditable: true,\n showOnlyCurrent: true,\n includeChildren: false,\n }\n },\n\n addProseMirrorPlugins() {\n return [createPlaceholderPlugin({ editor: this.editor, options: this.options })]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport { Plugin } from '@tiptap/pm/state'\n\nimport { DEFAULT_DATA_ATTRIBUTE, PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { buildPlaceholderDecorations } from '../utils/buildPlaceholderDecorations.js'\nimport { preparePlaceholderAttribute } from '../utils/preparePlaceholderAttribute.js'\nimport { createViewportPluginView, viewportPluginState } from '../utils/viewportTracking.js'\n\nexport type CreatePluginOptions = {\n editor: Editor\n options: PlaceholderOptions\n}\n\n/**\n * Creates the ProseMirror plugin that renders placeholder decorations.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @returns The configured placeholder plugin.\n */\nexport function createPlaceholderPlugin({ editor, options }: CreatePluginOptions) {\n const dataAttribute = options.dataAttribute\n ? `data-${preparePlaceholderAttribute(options.dataAttribute)}`\n : `data-${DEFAULT_DATA_ATTRIBUTE}`\n\n return new Plugin({\n key: PLUGIN_KEY,\n state: viewportPluginState,\n view: createViewportPluginView,\n props: {\n decorations: ({ doc, selection }) =>\n buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }),\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport { isNodeEmpty } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport type { Selection } from '@tiptap/pm/state'\nimport type { Decoration } from '@tiptap/pm/view'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { PlaceholderOptions } from '../types.js'\nimport { createPlaceholderDecoration } from './createPlaceholderDecoration.js'\n\nfunction resolveEmptyNodeClass(\n emptyNodeClass: PlaceholderOptions['emptyNodeClass'],\n props: { editor: Editor; node: Node; pos: number; hasAnchor: boolean },\n): string {\n return typeof emptyNodeClass === 'function' ? emptyNodeClass(props) : emptyNodeClass\n}\n\n/**\n * Builds the placeholder decorations for the current document state.\n * @param options.editor - The editor instance.\n * @param options.options - The resolved placeholder options.\n * @param options.dataAttribute - The prepared `data-*` attribute name.\n * @param options.doc - The current document node.\n * @param options.selection - The current selection.\n * @returns A decoration set, or `null` when no placeholders should be shown.\n */\nexport function buildPlaceholderDecorations({\n editor,\n options,\n dataAttribute,\n doc,\n selection,\n}: {\n editor: Editor\n options: PlaceholderOptions\n dataAttribute: string\n doc: Node\n selection: Selection\n}): DecorationSet | null {\n const active = editor.isEditable || !options.showOnlyWhenEditable\n\n if (!active) {\n return null\n }\n\n const { anchor } = selection\n const decorations: Decoration[] = []\n const isEmptyDoc = editor.isEmpty\n\n const useResolvedPath = options.showOnlyCurrent && !options.includeChildren\n\n if (useResolvedPath) {\n const resolved = doc.resolve(anchor)\n\n // When the selection spans the whole document (e.g. an `AllSelection`\n // after Cmd+A), the anchor resolves to the document level (depth 0). In\n // that case the relevant textblock is the node directly after the\n // position rather than an ancestor. otherwise the placeholder would\n // disappear after selecting all and deleting.\n const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter\n const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor\n\n if (node && node.type.isTextblock && isNodeEmpty(node)) {\n const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize\n\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos: nodeStart,\n hasAnchor,\n }),\n },\n node,\n pos: nodeStart,\n }),\n )\n }\n } else {\n const pluginState = PLUGIN_KEY.getState(editor.state)\n const from = pluginState?.topPos ?? 0\n const to = pluginState?.bottomPos ?? doc.content.size\n\n doc.nodesBetween(from, to, (node, pos) => {\n const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize\n const isEmpty = !node.isLeaf && isNodeEmpty(node)\n\n if (!node.type.isTextblock) {\n return options.includeChildren\n }\n\n if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {\n decorations.push(\n createPlaceholderDecoration({\n editor,\n isEmptyDoc,\n dataAttribute,\n hasAnchor,\n placeholder: options.placeholder,\n classes: {\n emptyEditor: options.emptyEditorClass,\n emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {\n editor,\n node,\n pos,\n hasAnchor,\n }),\n },\n node,\n pos,\n }),\n )\n }\n\n return options.includeChildren\n })\n }\n\n return DecorationSet.create(doc, decorations)\n}\n","import type { Editor } from '@tiptap/core'\nimport type { Node } from '@tiptap/pm/model'\nimport { Decoration } from '@tiptap/pm/view'\n\nimport type { PlaceholderOptions } from '../types.js'\n\n/**\n * Creates a ProseMirror node decoration that applies a placeholder\n * CSS class and data attribute to an empty node.\n * @param options.editor - The editor instance\n * @param options.pos - The position of the node in the document\n * @param options.node - The ProseMirror node\n * @param options.isEmptyDoc - Whether the entire document is empty\n * @param options.hasAnchor - Whether the selection anchor is within the node\n * @param options.dataAttribute - The data attribute name (e.g. `data-placeholder`)\n * @param options.classes - CSS classes for empty nodes and the empty editor\n * @param options.placeholder - The placeholder text or a function that returns it\n * @returns A ProseMirror node decoration with placeholder classes and data attribute\n */\nexport function createPlaceholderDecoration(options: {\n editor: Editor\n pos: number\n node: Node\n isEmptyDoc: boolean\n hasAnchor: boolean\n dataAttribute: string\n classes: {\n emptyEditor: PlaceholderOptions['emptyEditorClass']\n emptyNode: string\n }\n placeholder: PlaceholderOptions['placeholder']\n}) {\n const {\n editor,\n placeholder,\n dataAttribute,\n pos,\n node,\n isEmptyDoc,\n hasAnchor,\n classes: { emptyNode, emptyEditor },\n } = options\n const classes = [emptyNode]\n\n if (isEmptyDoc) {\n classes.push(emptyEditor)\n }\n\n return Decoration.node(pos, pos + node.nodeSize, {\n class: classes.join(' '),\n [dataAttribute]:\n typeof placeholder === 'function'\n ? placeholder({\n editor,\n node,\n pos,\n hasAnchor,\n })\n : placeholder,\n })\n}\n","/**\n * Prepares the placeholder attribute by ensuring it is properly formatted.\n * @param attr - The placeholder attribute string.\n * @returns The prepared placeholder attribute string.\n */\nexport function preparePlaceholderAttribute(attr: string): string {\n return (\n attr\n // replace whitespace with dashes\n .replace(/\\s+/g, '-')\n // replace non-alphanumeric characters\n // or special chars like $, %, &, etc.\n // but not dashes\n .replace(/[^a-zA-Z0-9-]/g, '')\n // and replace any numeric character at the start\n .replace(/^[0-9-]+/, '')\n // and finally replace any stray, leading dashes\n .replace(/^-+/, '')\n .toLowerCase()\n )\n}\n","/**\n * Checks if an element is scrollable by testing its overflow properties.\n * Elements with `overflow: hidden` or `overflow: clip` are intentionally\n * excluded — they clip content but don't emit scroll events.\n */\nfunction isScrollable(el: HTMLElement): boolean {\n const style = getComputedStyle(el)\n const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`\n\n return /auto|scroll|overlay/.test(overflow)\n}\n\nexport function findScrollParent(element: HTMLElement): HTMLElement | Window {\n let el: HTMLElement | null = element\n\n while (el) {\n if (isScrollable(el)) {\n return el\n }\n\n // Check if we hit a Shadow DOM boundary. If so, jump to the shadow host\n // and continue traversing the light DOM.\n const parent = el.parentElement\n if (!parent) {\n const root = el.getRootNode()\n if (root instanceof ShadowRoot) {\n el = root.host as HTMLElement\n continue\n }\n\n return window\n }\n\n el = parent\n }\n\n return window\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { VIEWPORT_OVERSCAN_PX } from '../constants.js'\n\nfunction getContainerRect(container: HTMLElement | Window): { top: number; bottom: number } {\n if (container === window) {\n return { top: 0, bottom: window.innerHeight }\n }\n\n return (container as HTMLElement).getBoundingClientRect()\n}\n\nexport function getViewportBoundaryPositions({\n doc,\n view,\n scrollContainer,\n}: {\n doc: Node\n view: EditorView\n scrollContainer?: HTMLElement | Window\n}) {\n const editorRect = view.dom.getBoundingClientRect()\n const containerRect = scrollContainer\n ? getContainerRect(scrollContainer)\n : { top: 0, bottom: window.innerHeight }\n\n const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX\n const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX\n\n if (visibleTop >= visibleBottom) {\n // Editor is not visible — fall back to full document range\n return { top: 0, bottom: doc.content.size }\n }\n\n // Pick the x-coordinate based on text direction. In LTR the content\n // starts at the left edge; in RTL it starts at the right edge.\n // Clamp to ensure the coordinate stays inside the editor bounds.\n const isRTL = getComputedStyle(view.dom).direction === 'rtl'\n const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2\n\n const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 })\n const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 })\n\n return {\n top: topPos ? topPos.pos : 0,\n bottom: bottomPos ? bottomPos.pos : doc.content.size,\n }\n}\n","import type { EditorState, PluginView, StateField, Transaction } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\n\nimport { PLUGIN_KEY } from '../constants.js'\nimport type { ViewportState } from '../types.js'\nimport { findScrollParent } from './findScrollParent.js'\nimport { getViewportBoundaryPositions } from './getViewportBoundaryPositions.js'\n\n/**\n * The plugin `state` config that tracks the visible viewport boundaries so the\n * decoration callback only scans the nodes currently on screen.\n */\nexport const viewportPluginState: StateField<ViewportState> = {\n /**\n * Initialises the viewport state with no known positions.\n * @returns The initial viewport state.\n */\n init(): ViewportState {\n return { topPos: null, bottomPos: null }\n },\n\n /**\n * Updates the viewport state from incoming transactions.\n * @param tr - The transaction being applied.\n * @param prev - The previous viewport state.\n * @returns The next viewport state.\n */\n apply(tr: Transaction, prev: ViewportState): ViewportState {\n const meta = tr.getMeta(PLUGIN_KEY) as\n | { positions?: { top: number; bottom: number } }\n | undefined\n\n if (meta?.positions) {\n return { topPos: meta.positions.top, bottomPos: meta.positions.bottom }\n }\n\n if (!tr.docChanged) {\n return prev\n }\n\n // Preserve last known viewport positions across transactions.\n // Without this, every keystroke resets back to a full document\n // scan, defeating the viewport optimisation.\n // Only map when we have actual positions — null means \"no viewport\n // info yet\" and should stay null to fall back to full doc scan.\n return {\n topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,\n bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null,\n }\n },\n}\n\n/**\n * Creates the plugin `view` that recomputes the visible viewport on scroll and\n * document size changes, dispatching the result into the plugin state.\n * @param view - The editor view the plugin is attached to.\n * @returns A plugin view with `update` and `destroy` handlers.\n */\nexport function createViewportPluginView(view: EditorView): PluginView {\n const scrollContainer = findScrollParent(view.dom)\n\n const computeAndDispatch = () => {\n const positions = getViewportBoundaryPositions({\n view,\n doc: view.state.doc,\n scrollContainer,\n })\n\n const prev = PLUGIN_KEY.getState(view.state)\n if (prev?.topPos === positions.top && prev?.bottomPos === positions.bottom) {\n return\n }\n\n const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions })\n view.dispatch(tr)\n }\n\n // rAF-based scheduler with interval guard (150ms → ~6–7 Hz) used by\n // scroll events and update(). The overscan margin hides the extra\n // latency. When the guard blocks, the callback reschedules itself so\n // the trailing position is always captured.\n let frame: number | null = null\n let lastCompute = 0\n const MIN_SCROLL_INTERVAL = 150\n\n const scheduleFrame = () => {\n if (frame !== null) return\n frame = requestAnimationFrame(() => {\n frame = null\n const now = performance.now()\n if (now - lastCompute >= MIN_SCROLL_INTERVAL) {\n lastCompute = now\n computeAndDispatch()\n } else {\n scheduleFrame()\n }\n })\n }\n\n scrollContainer.addEventListener('scroll', scheduleFrame, { passive: true })\n\n // Fire once to populate initial viewport\n computeAndDispatch()\n\n return {\n update(_view: EditorView, prevState: EditorState) {\n if (view.state.doc.content.size !== prevState.doc.content.size) {\n scheduleFrame()\n }\n },\n destroy: () => {\n if (frame !== null) {\n cancelAnimationFrame(frame)\n }\n scrollContainer.removeEventListener('scroll', scheduleFrame)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/extensions",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.0",
|
|
4
4
|
"description": "various extensions for tiptap",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tiptap",
|
|
@@ -100,12 +100,12 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@tiptap/core": "^3.
|
|
104
|
-
"@tiptap/pm": "^3.
|
|
103
|
+
"@tiptap/core": "^3.25.0",
|
|
104
|
+
"@tiptap/pm": "^3.25.0"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
|
-
"@tiptap/core": "3.
|
|
108
|
-
"@tiptap/pm": "3.
|
|
107
|
+
"@tiptap/core": "3.25.0",
|
|
108
|
+
"@tiptap/pm": "3.25.0"
|
|
109
109
|
},
|
|
110
110
|
"scripts": {
|
|
111
111
|
"build": "tsup"
|
package/src/placeholder/types.ts
CHANGED
|
@@ -20,9 +20,18 @@ export interface PlaceholderOptions {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* **The class name for empty nodes**
|
|
23
|
+
*
|
|
24
|
+
* You can use a function to return a dynamic class name or a string.
|
|
23
25
|
* @default 'is-empty'
|
|
24
26
|
*/
|
|
25
|
-
emptyNodeClass:
|
|
27
|
+
emptyNodeClass:
|
|
28
|
+
| ((EmptyNodeClassProps: {
|
|
29
|
+
editor: Editor
|
|
30
|
+
node: ProsemirrorNode
|
|
31
|
+
pos: number
|
|
32
|
+
hasAnchor: boolean
|
|
33
|
+
}) => string)
|
|
34
|
+
| string
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
37
|
* **The data-attribute used for the placeholder label**
|
|
@@ -9,6 +9,13 @@ import { PLUGIN_KEY } from '../constants.js'
|
|
|
9
9
|
import type { PlaceholderOptions } from '../types.js'
|
|
10
10
|
import { createPlaceholderDecoration } from './createPlaceholderDecoration.js'
|
|
11
11
|
|
|
12
|
+
function resolveEmptyNodeClass(
|
|
13
|
+
emptyNodeClass: PlaceholderOptions['emptyNodeClass'],
|
|
14
|
+
props: { editor: Editor; node: Node; pos: number; hasAnchor: boolean },
|
|
15
|
+
): string {
|
|
16
|
+
return typeof emptyNodeClass === 'function' ? emptyNodeClass(props) : emptyNodeClass
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Builds the placeholder decorations for the current document state.
|
|
14
21
|
* @param options.editor - The editor instance.
|
|
@@ -41,11 +48,6 @@ export function buildPlaceholderDecorations({
|
|
|
41
48
|
const decorations: Decoration[] = []
|
|
42
49
|
const isEmptyDoc = editor.isEmpty
|
|
43
50
|
|
|
44
|
-
const classes = {
|
|
45
|
-
emptyEditor: options.emptyEditorClass,
|
|
46
|
-
emptyNode: options.emptyNodeClass,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
51
|
const useResolvedPath = options.showOnlyCurrent && !options.includeChildren
|
|
50
52
|
|
|
51
53
|
if (useResolvedPath) {
|
|
@@ -69,7 +71,15 @@ export function buildPlaceholderDecorations({
|
|
|
69
71
|
dataAttribute,
|
|
70
72
|
hasAnchor,
|
|
71
73
|
placeholder: options.placeholder,
|
|
72
|
-
classes
|
|
74
|
+
classes: {
|
|
75
|
+
emptyEditor: options.emptyEditorClass,
|
|
76
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
77
|
+
editor,
|
|
78
|
+
node,
|
|
79
|
+
pos: nodeStart,
|
|
80
|
+
hasAnchor,
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
73
83
|
node,
|
|
74
84
|
pos: nodeStart,
|
|
75
85
|
}),
|
|
@@ -96,7 +106,15 @@ export function buildPlaceholderDecorations({
|
|
|
96
106
|
dataAttribute,
|
|
97
107
|
hasAnchor,
|
|
98
108
|
placeholder: options.placeholder,
|
|
99
|
-
classes
|
|
109
|
+
classes: {
|
|
110
|
+
emptyEditor: options.emptyEditorClass,
|
|
111
|
+
emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
|
|
112
|
+
editor,
|
|
113
|
+
node,
|
|
114
|
+
pos,
|
|
115
|
+
hasAnchor,
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
100
118
|
node,
|
|
101
119
|
pos,
|
|
102
120
|
}),
|
|
@@ -26,7 +26,7 @@ export function createPlaceholderDecoration(options: {
|
|
|
26
26
|
dataAttribute: string
|
|
27
27
|
classes: {
|
|
28
28
|
emptyEditor: PlaceholderOptions['emptyEditorClass']
|
|
29
|
-
emptyNode:
|
|
29
|
+
emptyNode: string
|
|
30
30
|
}
|
|
31
31
|
placeholder: PlaceholderOptions['placeholder']
|
|
32
32
|
}) {
|