@tiptap/extensions 3.27.1 → 3.27.3

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.js CHANGED
@@ -206,13 +206,13 @@ var Gapcursor = Extension4.create({
206
206
  import { PluginKey as PluginKey3 } from "@tiptap/pm/state";
207
207
  var DEFAULT_DATA_ATTRIBUTE = "placeholder";
208
208
  var PLUGIN_KEY = new PluginKey3("tiptap__placeholder");
209
- var VIEWPORT_OVERSCAN_PX = 200;
210
209
 
211
210
  // src/placeholder/placeholder.ts
212
211
  import { Extension as Extension5 } from "@tiptap/core";
213
212
 
214
213
  // src/placeholder/plugins/PlaceholderPlugin.ts
215
214
  import { Plugin as Plugin3 } from "@tiptap/pm/state";
215
+ import { DecorationSet as DecorationSet4 } from "@tiptap/pm/view";
216
216
 
217
217
  // src/placeholder/utils/buildPlaceholderDecorations.ts
218
218
  import { isNodeEmpty } from "@tiptap/core";
@@ -250,6 +250,50 @@ function createPlaceholderDecoration(options) {
250
250
  function resolveEmptyNodeClass(emptyNodeClass, props) {
251
251
  return typeof emptyNodeClass === "function" ? emptyNodeClass(props) : emptyNodeClass;
252
252
  }
253
+ function scanRangeForDecorations({
254
+ editor,
255
+ options,
256
+ dataAttribute,
257
+ doc,
258
+ selection,
259
+ from,
260
+ to
261
+ }) {
262
+ const { anchor } = selection;
263
+ const decorations = [];
264
+ const isEmptyDoc = editor.isEmpty;
265
+ doc.nodesBetween(from, to, (node, pos) => {
266
+ const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
267
+ const isEmpty = !node.isLeaf && isNodeEmpty(node);
268
+ if (!node.type.isTextblock) {
269
+ return options.includeChildren;
270
+ }
271
+ if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
272
+ decorations.push(
273
+ createPlaceholderDecoration({
274
+ editor,
275
+ isEmptyDoc,
276
+ dataAttribute,
277
+ hasAnchor,
278
+ placeholder: options.placeholder,
279
+ classes: {
280
+ emptyEditor: options.emptyEditorClass,
281
+ emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
282
+ editor,
283
+ node,
284
+ pos,
285
+ hasAnchor
286
+ })
287
+ },
288
+ node,
289
+ pos
290
+ })
291
+ );
292
+ }
293
+ return options.includeChildren;
294
+ });
295
+ return decorations;
296
+ }
253
297
  function buildPlaceholderDecorations({
254
298
  editor,
255
299
  options,
@@ -257,7 +301,6 @@ function buildPlaceholderDecorations({
257
301
  doc,
258
302
  selection
259
303
  }) {
260
- var _a, _b;
261
304
  const active = editor.isEditable || !options.showOnlyWhenEditable;
262
305
  if (!active) {
263
306
  return null;
@@ -294,212 +337,216 @@ function buildPlaceholderDecorations({
294
337
  );
295
338
  }
296
339
  } else {
297
- const pluginState = PLUGIN_KEY.getState(editor.state);
298
- const from = (_a = pluginState == null ? void 0 : pluginState.topPos) != null ? _a : 0;
299
- const to = (_b = pluginState == null ? void 0 : pluginState.bottomPos) != null ? _b : doc.content.size;
300
- doc.nodesBetween(from, to, (node, pos) => {
301
- const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
302
- const isEmpty = !node.isLeaf && isNodeEmpty(node);
303
- if (!node.type.isTextblock) {
304
- return options.includeChildren;
305
- }
306
- if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
307
- decorations.push(
308
- createPlaceholderDecoration({
309
- editor,
310
- isEmptyDoc,
311
- dataAttribute,
312
- hasAnchor,
313
- placeholder: options.placeholder,
314
- classes: {
315
- emptyEditor: options.emptyEditorClass,
316
- emptyNode: resolveEmptyNodeClass(options.emptyNodeClass, {
317
- editor,
318
- node,
319
- pos,
320
- hasAnchor
321
- })
322
- },
323
- node,
324
- pos
325
- })
326
- );
327
- }
328
- return options.includeChildren;
329
- });
340
+ decorations.push(
341
+ ...scanRangeForDecorations({
342
+ editor,
343
+ options,
344
+ dataAttribute,
345
+ doc,
346
+ selection,
347
+ from: 0,
348
+ to: doc.content.size
349
+ })
350
+ );
330
351
  }
331
352
  return DecorationSet2.create(doc, decorations);
332
353
  }
333
354
 
334
- // src/placeholder/utils/preparePlaceholderAttribute.ts
335
- function preparePlaceholderAttribute(attr) {
336
- return attr.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/^[0-9-]+/, "").replace(/^-+/, "").toLowerCase();
337
- }
355
+ // src/placeholder/utils/placeholderStateField.ts
356
+ import { getChangedRanges } from "@tiptap/core";
357
+ import { DecorationSet as DecorationSet3 } from "@tiptap/pm/view";
338
358
 
339
- // src/placeholder/utils/findScrollParent.ts
340
- function isScrollable(el) {
341
- const style = getComputedStyle(el);
342
- const overflow = `${style.overflow} ${style.overflowY} ${style.overflowX}`;
343
- return /auto|scroll|overlay/.test(overflow);
359
+ // src/placeholder/utils/resolveTopLevelRange.ts
360
+ function resolveTopLevelRange(doc, pos) {
361
+ var _a;
362
+ const resolved = doc.resolve(pos);
363
+ if (resolved.depth === 0) {
364
+ const node2 = (_a = resolved.nodeAfter) != null ? _a : resolved.nodeBefore;
365
+ if (!node2) {
366
+ return { from: pos, to: pos };
367
+ }
368
+ const nodePos = resolved.nodeAfter ? pos : pos - node2.nodeSize;
369
+ return { from: nodePos, to: nodePos + node2.nodeSize };
370
+ }
371
+ const topLevelPos = resolved.before(1);
372
+ const node = resolved.node(1);
373
+ return { from: topLevelPos, to: topLevelPos + node.nodeSize };
374
+ }
375
+ function toContentRelativeRange(doc, range) {
376
+ return {
377
+ from: Math.max(0, range.from - 1),
378
+ to: Math.min(doc.content.size, range.to - 1)
379
+ };
344
380
  }
345
- function findScrollParent(element) {
346
- let el = element;
347
- while (el) {
348
- if (isScrollable(el)) {
349
- return el;
381
+ function getTopLevelBlocksInRange(doc, from, to) {
382
+ const ranges = [];
383
+ doc.forEach((node, offset) => {
384
+ const nodeStart = offset;
385
+ const nodeEnd = nodeStart + node.nodeSize;
386
+ const absNodeStart = nodeStart + 1;
387
+ const absNodeEnd = nodeEnd + 1;
388
+ if (absNodeStart < to && absNodeEnd > from) {
389
+ ranges.push({ from: nodeStart, to: nodeEnd });
350
390
  }
351
- const parent = el.parentElement;
352
- if (!parent) {
353
- const root = el.getRootNode();
354
- if (root instanceof ShadowRoot) {
355
- el = root.host;
356
- continue;
357
- }
358
- return window;
391
+ });
392
+ return ranges;
393
+ }
394
+ function mergeRanges(ranges) {
395
+ if (ranges.length === 0) {
396
+ return [];
397
+ }
398
+ const sorted = [...ranges].sort((a, b) => a.from - b.from);
399
+ const merged = [{ ...sorted[0] }];
400
+ for (let i = 1; i < sorted.length; i += 1) {
401
+ const last = merged[merged.length - 1];
402
+ const current = sorted[i];
403
+ if (current.from <= last.to) {
404
+ last.to = Math.max(last.to, current.to);
405
+ } else {
406
+ merged.push({ ...current });
359
407
  }
360
- el = parent;
361
408
  }
362
- return window;
409
+ return merged;
363
410
  }
364
411
 
365
- // src/placeholder/utils/getViewportBoundaryPositions.ts
366
- function getContainerRect(container) {
367
- if (container === window) {
368
- return { top: 0, bottom: window.innerHeight };
412
+ // src/placeholder/utils/placeholderStateField.ts
413
+ function collectBlocksForChange(doc, change) {
414
+ const ranges = getTopLevelBlocksInRange(doc, change.from, change.to);
415
+ ranges.push(toContentRelativeRange(doc, resolveTopLevelRange(doc, change.from)));
416
+ if (change.to > change.from) {
417
+ ranges.push(
418
+ toContentRelativeRange(
419
+ doc,
420
+ resolveTopLevelRange(doc, Math.min(change.to, doc.content.size + 1) - 1)
421
+ )
422
+ );
423
+ } else if (change.from < doc.content.size + 1) {
424
+ ranges.push(
425
+ toContentRelativeRange(
426
+ doc,
427
+ resolveTopLevelRange(doc, Math.min(change.from + 1, doc.content.size))
428
+ )
429
+ );
369
430
  }
370
- return container.getBoundingClientRect();
431
+ return ranges;
371
432
  }
372
- function getViewportBoundaryPositions({
373
- view,
374
- scrollContainer
375
- }) {
376
- const editorRect = view.dom.getBoundingClientRect();
377
- if (editorRect.width <= 0 || editorRect.height <= 0) {
378
- return null;
379
- }
380
- const containerRect = scrollContainer ? getContainerRect(scrollContainer) : { top: 0, bottom: window.innerHeight };
381
- const visibleTop = Math.max(editorRect.top, containerRect.top) - VIEWPORT_OVERSCAN_PX;
382
- const visibleBottom = Math.min(editorRect.bottom, containerRect.bottom) + VIEWPORT_OVERSCAN_PX;
383
- if (visibleTop >= visibleBottom) {
384
- return null;
385
- }
386
- const minX = editorRect.left + 1;
387
- const maxX = editorRect.right - 1;
388
- if (minX > maxX) {
389
- return null;
390
- }
391
- const isRTL = getComputedStyle(view.dom).direction === "rtl";
392
- const targetX = isRTL ? editorRect.right - 2 : editorRect.left + 2;
393
- const x = Math.min(Math.max(targetX, minX), maxX);
394
- const probeTop = Math.max(visibleTop + 2, editorRect.top + 1);
395
- const probeBottom = Math.min(visibleBottom - 2, editorRect.bottom - 1);
396
- if (probeTop > probeBottom) {
397
- return null;
433
+ function collectRescanRanges(tr, oldState, newState) {
434
+ const ranges = [];
435
+ if (tr.docChanged) {
436
+ const changes = getChangedRanges(tr);
437
+ for (const change of changes) {
438
+ ranges.push(...collectBlocksForChange(newState.doc, change.newRange));
439
+ }
398
440
  }
399
- const topPos = view.posAtCoords({ left: x, top: probeTop });
400
- const bottomPos = view.posAtCoords({ left: x, top: probeBottom });
401
- if (!topPos || !bottomPos) {
402
- return null;
441
+ if (tr.selectionSet) {
442
+ ranges.push(
443
+ toContentRelativeRange(
444
+ newState.doc,
445
+ resolveTopLevelRange(newState.doc, tr.mapping.map(oldState.selection.anchor))
446
+ )
447
+ );
448
+ ranges.push(
449
+ toContentRelativeRange(
450
+ newState.doc,
451
+ resolveTopLevelRange(newState.doc, newState.selection.anchor)
452
+ )
453
+ );
403
454
  }
404
- return { top: topPos.pos, bottom: bottomPos.pos };
455
+ return mergeRanges(ranges);
405
456
  }
406
-
407
- // src/placeholder/utils/viewportTracking.ts
408
- var viewportPluginState = {
409
- /**
410
- * Initialises the viewport state with no known positions.
411
- * @returns The initial viewport state.
412
- */
413
- init() {
414
- return { topPos: null, bottomPos: null };
415
- },
416
- /**
417
- * Updates the viewport state from incoming transactions.
418
- * @param tr - The transaction being applied.
419
- * @param prev - The previous viewport state.
420
- * @returns The next viewport state.
421
- */
422
- apply(tr, prev) {
423
- const meta = tr.getMeta(PLUGIN_KEY);
424
- if (meta == null ? void 0 : meta.positions) {
425
- return { topPos: meta.positions.top, bottomPos: meta.positions.bottom };
426
- }
427
- if (!tr.docChanged) {
428
- return prev;
457
+ function clampRange(from, to, doc) {
458
+ const clampedFrom = Math.max(0, Math.min(from, doc.content.size));
459
+ const clampedTo = Math.max(clampedFrom, Math.min(to, doc.content.size));
460
+ return { from: clampedFrom, to: clampedTo };
461
+ }
462
+ function updateDecorationsInRanges({
463
+ decorations,
464
+ ranges,
465
+ editor,
466
+ options,
467
+ dataAttribute,
468
+ doc,
469
+ selection
470
+ }) {
471
+ let next = decorations;
472
+ for (const range of ranges) {
473
+ const { from, to } = clampRange(range.from, range.to, doc);
474
+ const existing = next.find(from, to).filter((decoration) => decoration.from >= from && decoration.to <= to);
475
+ if (existing.length) {
476
+ next = next.remove(existing);
429
477
  }
430
- return {
431
- topPos: prev.topPos !== null ? tr.mapping.map(prev.topPos) : null,
432
- bottomPos: prev.bottomPos !== null ? tr.mapping.map(prev.bottomPos) : null
433
- };
434
- }
435
- };
436
- function createViewportPluginView(view) {
437
- const scrollContainer = findScrollParent(view.dom);
438
- const computeAndDispatch = () => {
439
- const positions = getViewportBoundaryPositions({
440
- view,
441
- scrollContainer
478
+ const newDecos = scanRangeForDecorations({
479
+ editor,
480
+ options,
481
+ dataAttribute,
482
+ doc,
483
+ selection,
484
+ from,
485
+ to
442
486
  });
443
- if (positions === null) {
444
- return;
487
+ if (newDecos.length) {
488
+ next = next.add(doc, newDecos);
445
489
  }
446
- const prev = PLUGIN_KEY.getState(view.state);
447
- if ((prev == null ? void 0 : prev.topPos) === positions.top && (prev == null ? void 0 : prev.bottomPos) === positions.bottom) {
448
- return;
449
- }
450
- const tr = view.state.tr.setMeta(PLUGIN_KEY, { positions });
451
- view.dispatch(tr);
452
- };
453
- let frame = null;
454
- let lastCompute = 0;
455
- const MIN_SCROLL_INTERVAL = 150;
456
- const scheduleFrame = () => {
457
- if (frame !== null) return;
458
- frame = requestAnimationFrame(() => {
459
- frame = null;
460
- const now = performance.now();
461
- if (now - lastCompute >= MIN_SCROLL_INTERVAL) {
462
- lastCompute = now;
463
- computeAndDispatch();
464
- } else {
465
- scheduleFrame();
466
- }
467
- });
468
- };
469
- scrollContainer.addEventListener("scroll", scheduleFrame, { passive: true });
470
- const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(scheduleFrame) : null;
471
- resizeObserver == null ? void 0 : resizeObserver.observe(view.dom);
472
- const intersectionObserver = typeof IntersectionObserver !== "undefined" ? new IntersectionObserver(scheduleFrame) : null;
473
- intersectionObserver == null ? void 0 : intersectionObserver.observe(view.dom);
474
- view.dom.addEventListener("focus", scheduleFrame);
475
- computeAndDispatch();
490
+ }
491
+ return next;
492
+ }
493
+ function createPlaceholderStateField({
494
+ editor,
495
+ options,
496
+ dataAttribute
497
+ }) {
476
498
  return {
477
- update(_view, prevState) {
478
- if (view.state.doc.content.size !== prevState.doc.content.size) {
479
- scheduleFrame();
480
- }
499
+ init(_config, state) {
500
+ const decorations = buildPlaceholderDecorations({
501
+ editor,
502
+ options,
503
+ dataAttribute,
504
+ doc: state.doc,
505
+ selection: state.selection
506
+ });
507
+ return decorations != null ? decorations : DecorationSet3.empty;
481
508
  },
482
- destroy: () => {
483
- if (frame !== null) {
484
- cancelAnimationFrame(frame);
509
+ apply(tr, prev, oldState, newState) {
510
+ if (!tr.docChanged && !tr.selectionSet) {
511
+ return prev;
485
512
  }
486
- scrollContainer.removeEventListener("scroll", scheduleFrame);
487
- resizeObserver == null ? void 0 : resizeObserver.disconnect();
488
- intersectionObserver == null ? void 0 : intersectionObserver.disconnect();
489
- view.dom.removeEventListener("focus", scheduleFrame);
513
+ const mapped = prev.map(tr.mapping, tr.doc);
514
+ const ranges = collectRescanRanges(tr, oldState, newState);
515
+ return updateDecorationsInRanges({
516
+ decorations: mapped,
517
+ ranges,
518
+ editor,
519
+ options,
520
+ dataAttribute,
521
+ doc: newState.doc,
522
+ selection: newState.selection
523
+ });
490
524
  }
491
525
  };
492
526
  }
493
527
 
528
+ // src/placeholder/utils/preparePlaceholderAttribute.ts
529
+ function preparePlaceholderAttribute(attr) {
530
+ return attr.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").replace(/^[0-9-]+/, "").replace(/^-+/, "").toLowerCase();
531
+ }
532
+
494
533
  // src/placeholder/plugins/PlaceholderPlugin.ts
495
534
  function createPlaceholderPlugin({ editor, options }) {
496
535
  const dataAttribute = options.dataAttribute ? `data-${preparePlaceholderAttribute(options.dataAttribute)}` : `data-${DEFAULT_DATA_ATTRIBUTE}`;
536
+ const useResolvedPath = options.showOnlyCurrent && !options.includeChildren;
497
537
  return new Plugin3({
498
538
  key: PLUGIN_KEY,
499
- state: viewportPluginState,
500
- view: createViewportPluginView,
539
+ ...useResolvedPath ? {} : {
540
+ state: createPlaceholderStateField({ editor, options, dataAttribute })
541
+ },
501
542
  props: {
502
- decorations: ({ doc, selection }) => buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection })
543
+ decorations: useResolvedPath ? ({ doc, selection }) => buildPlaceholderDecorations({ editor, options, dataAttribute, doc, selection }) : (state) => {
544
+ var _a;
545
+ if (options.showOnlyWhenEditable && !editor.isEditable) {
546
+ return DecorationSet4.empty;
547
+ }
548
+ return (_a = PLUGIN_KEY.getState(state)) != null ? _a : DecorationSet4.empty;
549
+ }
503
550
  }
504
551
  });
505
552
  }
@@ -526,7 +573,7 @@ var Placeholder = Extension5.create({
526
573
  // src/selection/selection.ts
527
574
  import { createStyleTag, Extension as Extension6, isNodeSelection } from "@tiptap/core";
528
575
  import { Plugin as Plugin4, PluginKey as PluginKey4 } from "@tiptap/pm/state";
529
- import { Decoration as Decoration3, DecorationSet as DecorationSet3 } from "@tiptap/pm/view";
576
+ import { Decoration as Decoration3, DecorationSet as DecorationSet5 } from "@tiptap/pm/view";
530
577
  var selectionStyle = `.ProseMirror:not(.ProseMirror-focused) *::selection {
531
578
  background: transparent;
532
579
  }
@@ -554,7 +601,7 @@ var Selection = Extension6.create({
554
601
  if (state.selection.empty || editor.isFocused || !editor.isEditable || isNodeSelection(state.selection) || editor.view.dragging) {
555
602
  return null;
556
603
  }
557
- return DecorationSet3.create(state.doc, [
604
+ return DecorationSet5.create(state.doc, [
558
605
  Decoration3.inline(state.selection.from, state.selection.to, {
559
606
  class: options.className
560
607
  })