@tiptap/extensions 3.23.5 → 3.24.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.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Extension, ParentConfig, Editor } from '@tiptap/core';
2
2
  import { Node } from '@tiptap/pm/model';
3
+ import { PluginKey } from '@tiptap/pm/state';
3
4
 
4
5
  interface CharacterCountOptions {
5
6
  /**
@@ -142,11 +143,14 @@ declare module '@tiptap/core' {
142
143
  declare const Gapcursor: Extension<any, any>;
143
144
 
144
145
  /**
145
- * Prepares the placeholder attribute by ensuring it is properly formatted.
146
- * @param attr - The placeholder attribute string.
147
- * @returns The prepared placeholder attribute string.
146
+ * The viewport positions tracked by the placeholder plugin.
147
+ * `null` means "no viewport info yet" — the decoration callback falls back to
148
+ * a full document scan until the scroll handler fires.
148
149
  */
149
- declare function preparePlaceholderAttribute(attr: string): string;
150
+ interface ViewportState {
151
+ topPos: number | null;
152
+ bottomPos: number | null;
153
+ }
150
154
  interface PlaceholderOptions {
151
155
  /**
152
156
  * **The class name for the empty editor**
@@ -193,14 +197,20 @@ interface PlaceholderOptions {
193
197
  */
194
198
  showOnlyCurrent: boolean;
195
199
  /**
196
- * **Controls if the placeholder should be shown for all descendents.**
200
+ * **Controls if the placeholder should be shown for all descendants.**
197
201
  *
198
- * If true, the placeholder will be shown for all descendents.
202
+ * If true, the placeholder will be shown for all descendants.
199
203
  * If false, the placeholder will only be shown for the current node.
200
204
  * @default false
201
205
  */
202
206
  includeChildren: boolean;
203
207
  }
208
+
209
+ /** The default data attribute label */
210
+ declare const DEFAULT_DATA_ATTRIBUTE = "placeholder";
211
+ /** The plugin key used to store and read the placeholder viewport state */
212
+ declare const PLUGIN_KEY: PluginKey<ViewportState>;
213
+
204
214
  /**
205
215
  * This extension allows you to add a placeholder to your editor.
206
216
  * A placeholder is a text that appears when the editor or a node is empty.
@@ -208,6 +218,13 @@ interface PlaceholderOptions {
208
218
  */
209
219
  declare const Placeholder: Extension<PlaceholderOptions, any>;
210
220
 
221
+ /**
222
+ * Prepares the placeholder attribute by ensuring it is properly formatted.
223
+ * @param attr - The placeholder attribute string.
224
+ * @returns The prepared placeholder attribute string.
225
+ */
226
+ declare function preparePlaceholderAttribute(attr: string): string;
227
+
211
228
  type SelectionOptions = {
212
229
  /**
213
230
  * The class name that should be added to the selected text.
@@ -289,4 +306,4 @@ declare module '@tiptap/core' {
289
306
  */
290
307
  declare const UndoRedo: Extension<UndoRedoOptions, any>;
291
308
 
292
- export { CharacterCount, type CharacterCountOptions, type CharacterCountStorage, Dropcursor, type DropcursorOptions, Focus, type FocusOptions, Gapcursor, Placeholder, type PlaceholderOptions, Selection, type SelectionOptions, TrailingNode, type TrailingNodeOptions, UndoRedo, type UndoRedoOptions, preparePlaceholderAttribute, skipTrailingNodeMeta };
309
+ export { CharacterCount, type CharacterCountOptions, type CharacterCountStorage, DEFAULT_DATA_ATTRIBUTE, Dropcursor, type DropcursorOptions, Focus, type FocusOptions, Gapcursor, PLUGIN_KEY, Placeholder, type PlaceholderOptions, Selection, type SelectionOptions, TrailingNode, type TrailingNodeOptions, UndoRedo, type UndoRedoOptions, type ViewportState, preparePlaceholderAttribute, skipTrailingNodeMeta };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Extension, ParentConfig, Editor } from '@tiptap/core';
2
2
  import { Node } from '@tiptap/pm/model';
3
+ import { PluginKey } from '@tiptap/pm/state';
3
4
 
4
5
  interface CharacterCountOptions {
5
6
  /**
@@ -142,11 +143,14 @@ declare module '@tiptap/core' {
142
143
  declare const Gapcursor: Extension<any, any>;
143
144
 
144
145
  /**
145
- * Prepares the placeholder attribute by ensuring it is properly formatted.
146
- * @param attr - The placeholder attribute string.
147
- * @returns The prepared placeholder attribute string.
146
+ * The viewport positions tracked by the placeholder plugin.
147
+ * `null` means "no viewport info yet" — the decoration callback falls back to
148
+ * a full document scan until the scroll handler fires.
148
149
  */
149
- declare function preparePlaceholderAttribute(attr: string): string;
150
+ interface ViewportState {
151
+ topPos: number | null;
152
+ bottomPos: number | null;
153
+ }
150
154
  interface PlaceholderOptions {
151
155
  /**
152
156
  * **The class name for the empty editor**
@@ -193,14 +197,20 @@ interface PlaceholderOptions {
193
197
  */
194
198
  showOnlyCurrent: boolean;
195
199
  /**
196
- * **Controls if the placeholder should be shown for all descendents.**
200
+ * **Controls if the placeholder should be shown for all descendants.**
197
201
  *
198
- * If true, the placeholder will be shown for all descendents.
202
+ * If true, the placeholder will be shown for all descendants.
199
203
  * If false, the placeholder will only be shown for the current node.
200
204
  * @default false
201
205
  */
202
206
  includeChildren: boolean;
203
207
  }
208
+
209
+ /** The default data attribute label */
210
+ declare const DEFAULT_DATA_ATTRIBUTE = "placeholder";
211
+ /** The plugin key used to store and read the placeholder viewport state */
212
+ declare const PLUGIN_KEY: PluginKey<ViewportState>;
213
+
204
214
  /**
205
215
  * This extension allows you to add a placeholder to your editor.
206
216
  * A placeholder is a text that appears when the editor or a node is empty.
@@ -208,6 +218,13 @@ interface PlaceholderOptions {
208
218
  */
209
219
  declare const Placeholder: Extension<PlaceholderOptions, any>;
210
220
 
221
+ /**
222
+ * Prepares the placeholder attribute by ensuring it is properly formatted.
223
+ * @param attr - The placeholder attribute string.
224
+ * @returns The prepared placeholder attribute string.
225
+ */
226
+ declare function preparePlaceholderAttribute(attr: string): string;
227
+
211
228
  type SelectionOptions = {
212
229
  /**
213
230
  * The class name that should be added to the selected text.
@@ -289,4 +306,4 @@ declare module '@tiptap/core' {
289
306
  */
290
307
  declare const UndoRedo: Extension<UndoRedoOptions, any>;
291
308
 
292
- export { CharacterCount, type CharacterCountOptions, type CharacterCountStorage, Dropcursor, type DropcursorOptions, Focus, type FocusOptions, Gapcursor, Placeholder, type PlaceholderOptions, Selection, type SelectionOptions, TrailingNode, type TrailingNodeOptions, UndoRedo, type UndoRedoOptions, preparePlaceholderAttribute, skipTrailingNodeMeta };
309
+ export { CharacterCount, type CharacterCountOptions, type CharacterCountStorage, DEFAULT_DATA_ATTRIBUTE, Dropcursor, type DropcursorOptions, Focus, type FocusOptions, Gapcursor, PLUGIN_KEY, Placeholder, type PlaceholderOptions, Selection, type SelectionOptions, TrailingNode, type TrailingNodeOptions, UndoRedo, type UndoRedoOptions, type ViewportState, preparePlaceholderAttribute, skipTrailingNodeMeta };
package/dist/index.js CHANGED
@@ -202,14 +202,271 @@ var Gapcursor = Extension4.create({
202
202
  }
203
203
  });
204
204
 
205
- // src/placeholder/placeholder.ts
206
- import { Extension as Extension5, isNodeEmpty } from "@tiptap/core";
207
- import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state";
208
- import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
205
+ // src/placeholder/constants.ts
206
+ import { PluginKey as PluginKey3 } from "@tiptap/pm/state";
209
207
  var DEFAULT_DATA_ATTRIBUTE = "placeholder";
208
+ var PLUGIN_KEY = new PluginKey3("tiptap__placeholder");
209
+ var VIEWPORT_OVERSCAN_PX = 200;
210
+
211
+ // src/placeholder/placeholder.ts
212
+ import { Extension as Extension5 } from "@tiptap/core";
213
+
214
+ // src/placeholder/plugins/PlaceholderPlugin.ts
215
+ import { Plugin as Plugin3 } from "@tiptap/pm/state";
216
+
217
+ // src/placeholder/utils/buildPlaceholderDecorations.ts
218
+ import { isNodeEmpty } from "@tiptap/core";
219
+ import { DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
220
+
221
+ // src/placeholder/utils/createPlaceholderDecoration.ts
222
+ import { Decoration as Decoration2 } from "@tiptap/pm/view";
223
+ function createPlaceholderDecoration(options) {
224
+ const {
225
+ editor,
226
+ placeholder,
227
+ dataAttribute,
228
+ pos,
229
+ node,
230
+ isEmptyDoc,
231
+ hasAnchor,
232
+ classes: { emptyNode, emptyEditor }
233
+ } = options;
234
+ const classes = [emptyNode];
235
+ if (isEmptyDoc) {
236
+ classes.push(emptyEditor);
237
+ }
238
+ return Decoration2.node(pos, pos + node.nodeSize, {
239
+ class: classes.join(" "),
240
+ [dataAttribute]: typeof placeholder === "function" ? placeholder({
241
+ editor,
242
+ node,
243
+ pos,
244
+ hasAnchor
245
+ }) : placeholder
246
+ });
247
+ }
248
+
249
+ // src/placeholder/utils/buildPlaceholderDecorations.ts
250
+ function buildPlaceholderDecorations({
251
+ editor,
252
+ options,
253
+ dataAttribute,
254
+ doc,
255
+ selection
256
+ }) {
257
+ var _a, _b;
258
+ const active = editor.isEditable || !options.showOnlyWhenEditable;
259
+ if (!active) {
260
+ return null;
261
+ }
262
+ const { anchor } = selection;
263
+ const decorations = [];
264
+ const isEmptyDoc = editor.isEmpty;
265
+ const classes = {
266
+ emptyEditor: options.emptyEditorClass,
267
+ emptyNode: options.emptyNodeClass
268
+ };
269
+ const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
270
+ if (useResolvedPath) {
271
+ const resolved = doc.resolve(anchor);
272
+ const node = resolved.depth > 0 ? resolved.node(1) : resolved.nodeAfter;
273
+ const nodeStart = resolved.depth > 0 ? resolved.before(1) : anchor;
274
+ if (node && node.type.isTextblock && isNodeEmpty(node)) {
275
+ const hasAnchor = anchor >= nodeStart && anchor <= nodeStart + node.nodeSize;
276
+ decorations.push(
277
+ createPlaceholderDecoration({
278
+ editor,
279
+ isEmptyDoc,
280
+ dataAttribute,
281
+ hasAnchor,
282
+ placeholder: options.placeholder,
283
+ classes,
284
+ node,
285
+ pos: nodeStart
286
+ })
287
+ );
288
+ }
289
+ } else {
290
+ const pluginState = PLUGIN_KEY.getState(editor.state);
291
+ const from = (_a = pluginState == null ? void 0 : pluginState.topPos) != null ? _a : 0;
292
+ const to = (_b = pluginState == null ? void 0 : pluginState.bottomPos) != null ? _b : doc.content.size;
293
+ doc.nodesBetween(from, to, (node, pos) => {
294
+ const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
295
+ const isEmpty = !node.isLeaf && isNodeEmpty(node);
296
+ if (!node.type.isTextblock) {
297
+ return options.includeChildren;
298
+ }
299
+ if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
300
+ decorations.push(
301
+ createPlaceholderDecoration({
302
+ editor,
303
+ isEmptyDoc,
304
+ dataAttribute,
305
+ hasAnchor,
306
+ placeholder: options.placeholder,
307
+ classes,
308
+ node,
309
+ pos
310
+ })
311
+ );
312
+ }
313
+ return options.includeChildren;
314
+ });
315
+ }
316
+ return DecorationSet2.create(doc, decorations);
317
+ }
318
+
319
+ // src/placeholder/utils/preparePlaceholderAttribute.ts
210
320
  function preparePlaceholderAttribute(attr) {
211
321
  return attr.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/^[0-9-]+/, "").replace(/^-+/, "").toLowerCase();
212
322
  }
323
+
324
+ // src/placeholder/utils/findScrollParent.ts
325
+ function isScrollable(el) {
326
+ const style = getComputedStyle(el);
327
+ const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`;
328
+ return /auto|scroll|overlay/.test(overflow);
329
+ }
330
+ function findScrollParent(element) {
331
+ let el = element;
332
+ while (el) {
333
+ if (isScrollable(el)) {
334
+ return el;
335
+ }
336
+ const parent = el.parentElement;
337
+ if (!parent) {
338
+ const root = el.getRootNode();
339
+ if (root instanceof ShadowRoot) {
340
+ el = root.host;
341
+ continue;
342
+ }
343
+ return window;
344
+ }
345
+ el = parent;
346
+ }
347
+ return window;
348
+ }
349
+
350
+ // src/placeholder/utils/getViewportBoundaryPositions.ts
351
+ function getContainerRect(container) {
352
+ if (container === window) {
353
+ return { top: 0, bottom: window.innerHeight };
354
+ }
355
+ return container.getBoundingClientRect();
356
+ }
357
+ function getViewportBoundaryPositions({
358
+ doc,
359
+ view,
360
+ scrollContainer
361
+ }) {
362
+ const editorRect = view.dom.getBoundingClientRect();
363
+ const containerRect = scrollContainer ? getContainerRect(scrollContainer) : { top: 0, bottom: window.innerHeight };
364
+ const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX;
365
+ const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX;
366
+ if (visibleTop >= visibleBottom) {
367
+ return { top: 0, bottom: doc.content.size };
368
+ }
369
+ const isRTL = getComputedStyle(view.dom).direction === "rtl";
370
+ const x = isRTL ? Math.max(editorRect.right - 2, editorRect.left + 2) : editorRect.left + 2;
371
+ const topPos = view.posAtCoords({ left: x, top: visibleTop + 2 });
372
+ const bottomPos = view.posAtCoords({ left: x, top: visibleBottom - 2 });
373
+ return {
374
+ top: topPos ? topPos.pos : 0,
375
+ bottom: bottomPos ? bottomPos.pos : doc.content.size
376
+ };
377
+ }
378
+
379
+ // src/placeholder/utils/viewportTracking.ts
380
+ var viewportPluginState = {
381
+ /**
382
+ * Initialises the viewport state with no known positions.
383
+ * @returns The initial viewport state.
384
+ */
385
+ init() {
386
+ return { topPos: null, bottomPos: null };
387
+ },
388
+ /**
389
+ * Updates the viewport state from incoming transactions.
390
+ * @param tr - The transaction being applied.
391
+ * @param prev - The previous viewport state.
392
+ * @returns The next viewport state.
393
+ */
394
+ apply(tr, prev) {
395
+ const meta = tr.getMeta(PLUGIN_KEY);
396
+ if (meta == null ? void 0 : meta.positions) {
397
+ return { topPos: meta.positions.top, bottomPos: meta.positions.bottom };
398
+ }
399
+ if (!tr.docChanged) {
400
+ return prev;
401
+ }
402
+ return {
403
+ topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,
404
+ bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null
405
+ };
406
+ }
407
+ };
408
+ function createViewportPluginView(view) {
409
+ const scrollContainer = findScrollParent(view.dom);
410
+ const computeAndDispatch = () => {
411
+ const positions = getViewportBoundaryPositions({
412
+ view,
413
+ doc: view.state.doc,
414
+ scrollContainer
415
+ });
416
+ const prev = PLUGIN_KEY.getState(view.state);
417
+ if ((prev == null ? void 0 : prev.topPos) === positions.top && (prev == null ? void 0 : prev.bottomPos) === positions.bottom) {
418
+ return;
419
+ }
420
+ const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions });
421
+ view.dispatch(tr);
422
+ };
423
+ let frame = null;
424
+ let lastCompute = 0;
425
+ const MIN_SCROLL_INTERVAL = 150;
426
+ const scheduleFrame = () => {
427
+ if (frame !== null) return;
428
+ frame = requestAnimationFrame(() => {
429
+ frame = null;
430
+ const now = performance.now();
431
+ if (now - lastCompute >= MIN_SCROLL_INTERVAL) {
432
+ lastCompute = now;
433
+ computeAndDispatch();
434
+ } else {
435
+ scheduleFrame();
436
+ }
437
+ });
438
+ };
439
+ scrollContainer.addEventListener("scroll", scheduleFrame, { passive: true });
440
+ computeAndDispatch();
441
+ return {
442
+ update(_view, prevState) {
443
+ if (view.state.doc.content.size !== prevState.doc.content.size) {
444
+ scheduleFrame();
445
+ }
446
+ },
447
+ destroy: () => {
448
+ if (frame !== null) {
449
+ cancelAnimationFrame(frame);
450
+ }
451
+ scrollContainer.removeEventListener("scroll", scheduleFrame);
452
+ }
453
+ };
454
+ }
455
+
456
+ // src/placeholder/plugins/PlaceholderPlugin.ts
457
+ function createPlaceholderPlugin({ editor, options }) {
458
+ const dataAttribute = options.dataAttribute ? `data-${preparePlaceholderAttribute(options.dataAttribute)}` : `data-${DEFAULT_DATA_ATTRIBUTE}`;
459
+ return new Plugin3({
460
+ key: PLUGIN_KEY,
461
+ state: viewportPluginState,
462
+ view: createViewportPluginView,
463
+ props: {
464
+ decorations: ({ doc, selection }) => buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection })
465
+ }
466
+ });
467
+ }
468
+
469
+ // src/placeholder/placeholder.ts
213
470
  var Placeholder = Extension5.create({
214
471
  name: "placeholder",
215
472
  addOptions() {
@@ -224,48 +481,7 @@ var Placeholder = Extension5.create({
224
481
  };
225
482
  },
226
483
  addProseMirrorPlugins() {
227
- const dataAttribute = this.options.dataAttribute ? `data-${preparePlaceholderAttribute(this.options.dataAttribute)}` : `data-${DEFAULT_DATA_ATTRIBUTE}`;
228
- return [
229
- new Plugin3({
230
- key: new PluginKey3("placeholder"),
231
- props: {
232
- decorations: ({ doc, selection }) => {
233
- const active = this.editor.isEditable || !this.options.showOnlyWhenEditable;
234
- const { anchor } = selection;
235
- const decorations = [];
236
- if (!active) {
237
- return null;
238
- }
239
- const isEmptyDoc = this.editor.isEmpty;
240
- doc.descendants((node, pos) => {
241
- const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
242
- const isEmpty = !node.isLeaf && isNodeEmpty(node);
243
- if (!node.type.isTextblock) {
244
- return this.options.includeChildren;
245
- }
246
- if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
247
- const classes = [this.options.emptyNodeClass];
248
- if (isEmptyDoc) {
249
- classes.push(this.options.emptyEditorClass);
250
- }
251
- const decoration = Decoration2.node(pos, pos + node.nodeSize, {
252
- class: classes.join(" "),
253
- [dataAttribute]: typeof this.options.placeholder === "function" ? this.options.placeholder({
254
- editor: this.editor,
255
- node,
256
- pos,
257
- hasAnchor
258
- }) : this.options.placeholder
259
- });
260
- decorations.push(decoration);
261
- }
262
- return this.options.includeChildren;
263
- });
264
- return DecorationSet2.create(doc, decorations);
265
- }
266
- }
267
- })
268
- ];
484
+ return [createPlaceholderPlugin({ editor: this.editor, options: this.options })];
269
485
  }
270
486
  });
271
487
 
@@ -306,7 +522,10 @@ var Selection = Extension6.create({
306
522
  import { Extension as Extension7 } from "@tiptap/core";
307
523
  import { Plugin as Plugin5, PluginKey as PluginKey5 } from "@tiptap/pm/state";
308
524
  var skipTrailingNodeMeta = "skipTrailingNode";
309
- function nodeEqualsType({ types, node }) {
525
+ function nodeEqualsType({
526
+ types,
527
+ node
528
+ }) {
310
529
  return node && Array.isArray(types) && types.includes(node.type) || (node == null ? void 0 : node.type) === types;
311
530
  }
312
531
  var TrailingNode = Extension7.create({
@@ -396,9 +615,11 @@ var UndoRedo = Extension8.create({
396
615
  });
397
616
  export {
398
617
  CharacterCount,
618
+ DEFAULT_DATA_ATTRIBUTE,
399
619
  Dropcursor,
400
620
  Focus,
401
621
  Gapcursor,
622
+ PLUGIN_KEY,
402
623
  Placeholder,
403
624
  Selection,
404
625
  TrailingNode,