@magic-marker/prosemirror-suggest-changes 0.3.3-wrap-unwrap.29 → 0.4.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.
Files changed (88) hide show
  1. package/dist/__tests__/playwrightHelpers.d.ts +2 -2
  2. package/dist/__tests__/playwrightPage.d.ts +2 -50
  3. package/dist/commands.js +43 -178
  4. package/dist/ensureSelectionPlugin.js +3 -3
  5. package/dist/features/joinOnDelete/index.d.ts +19 -4
  6. package/dist/features/joinOnDelete/index.js +53 -166
  7. package/dist/generateId.js +4 -31
  8. package/dist/index.d.ts +2 -5
  9. package/dist/index.js +2 -4
  10. package/dist/replaceStep.d.ts +1 -1
  11. package/dist/schema.d.ts +1 -2
  12. package/dist/schema.js +1 -37
  13. package/dist/testing/testBuilders.d.ts +2 -1
  14. package/dist/utils.d.ts +0 -1
  15. package/dist/utils.js +2 -6
  16. package/dist/withSuggestChanges.d.ts +14 -11
  17. package/dist/withSuggestChanges.js +94 -64
  18. package/package.json +1 -1
  19. package/src/features/joinOnDelete/README.md +8 -0
  20. package/dist/features/joinOnDelete/__tests__/joinOnDeleteInLists.playwright.test.d.ts +0 -1
  21. package/dist/features/joinOnDelete/__tests__/joinOnDeleteInListsTipTapStyle.playwright.test.d.ts +0 -1
  22. package/dist/features/joinOnDelete/__tests__/listWithJoinsAndStructureMarks.playwright.test.d.ts +0 -1
  23. package/dist/features/joinOnDelete/normalizeJoinNodesMetadata.d.ts +0 -6
  24. package/dist/features/joinOnDelete/normalizeJoinNodesMetadata.js +0 -24
  25. package/dist/features/joinOnDelete/types.d.ts +0 -34
  26. package/dist/features/joinOnDelete/types.js +0 -20
  27. package/dist/features/transactionShaping/detectSpecialTransactionShape.d.ts +0 -3
  28. package/dist/features/transactionShaping/detectSpecialTransactionShape.js +0 -4
  29. package/dist/features/transactionShaping/index.d.ts +0 -3
  30. package/dist/features/transactionShaping/index.js +0 -11
  31. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.d.ts +0 -3
  32. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.js +0 -48
  33. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.test.d.ts +0 -1
  34. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.test.js +0 -188
  35. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/handleTipTapParagraphIntoListJoin.d.ts +0 -3
  36. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/handleTipTapParagraphIntoListJoin.js +0 -64
  37. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/index.d.ts +0 -2
  38. package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/index.js +0 -2
  39. package/dist/features/transactionShaping/types.d.ts +0 -20
  40. package/dist/features/transactionShaping/types.js +0 -1
  41. package/dist/features/wrapUnwrap/__tests__/blockquoteStructure.playwright.test.d.ts +0 -1
  42. package/dist/features/wrapUnwrap/__tests__/buildMaterializedPaths.test.d.ts +0 -1
  43. package/dist/features/wrapUnwrap/__tests__/listStructure.playwright.test.d.ts +0 -1
  44. package/dist/features/wrapUnwrap/__tests__/listStructureTextEdits.playwright.test.d.ts +0 -1
  45. package/dist/features/wrapUnwrap/__tests__/splitDetection.test.d.ts +0 -1
  46. package/dist/features/wrapUnwrap/addIdAttr.d.ts +0 -2
  47. package/dist/features/wrapUnwrap/addIdAttr.js +0 -60
  48. package/dist/features/wrapUnwrap/apply/applyStructureSuggestions.d.ts +0 -10
  49. package/dist/features/wrapUnwrap/apply/applyStructureSuggestions.js +0 -41
  50. package/dist/features/wrapUnwrap/apply/index.d.ts +0 -5
  51. package/dist/features/wrapUnwrap/apply/index.js +0 -21
  52. package/dist/features/wrapUnwrap/areEquivalentStructureMarks.d.ts +0 -2
  53. package/dist/features/wrapUnwrap/areEquivalentStructureMarks.js +0 -17
  54. package/dist/features/wrapUnwrap/buildMaterializedPaths.d.ts +0 -3
  55. package/dist/features/wrapUnwrap/buildMaterializedPaths.js +0 -82
  56. package/dist/features/wrapUnwrap/constants.d.ts +0 -3
  57. package/dist/features/wrapUnwrap/constants.js +0 -3
  58. package/dist/features/wrapUnwrap/generateUniqueNodeId.d.ts +0 -1
  59. package/dist/features/wrapUnwrap/generateUniqueNodeId.js +0 -6
  60. package/dist/features/wrapUnwrap/getNodeId.d.ts +0 -2
  61. package/dist/features/wrapUnwrap/getNodeId.js +0 -5
  62. package/dist/features/wrapUnwrap/revert/deleteNodeUpwards.d.ts +0 -3
  63. package/dist/features/wrapUnwrap/revert/deleteNodeUpwards.js +0 -37
  64. package/dist/features/wrapUnwrap/revert/index.d.ts +0 -5
  65. package/dist/features/wrapUnwrap/revert/index.js +0 -19
  66. package/dist/features/wrapUnwrap/revert/revertAddOp.d.ts +0 -4
  67. package/dist/features/wrapUnwrap/revert/revertAddOp.js +0 -4
  68. package/dist/features/wrapUnwrap/revert/revertMoveOp.d.ts +0 -16
  69. package/dist/features/wrapUnwrap/revert/revertMoveOp.js +0 -122
  70. package/dist/features/wrapUnwrap/revert/revertStructureSuggestions.d.ts +0 -10
  71. package/dist/features/wrapUnwrap/revert/revertStructureSuggestions.js +0 -236
  72. package/dist/features/wrapUnwrap/sameParentChain.d.ts +0 -2
  73. package/dist/features/wrapUnwrap/sameParentChain.js +0 -4
  74. package/dist/features/wrapUnwrap/structureChangesPlugin.d.ts +0 -17
  75. package/dist/features/wrapUnwrap/structureChangesPlugin.js +0 -299
  76. package/dist/features/wrapUnwrap/types.d.ts +0 -54
  77. package/dist/features/wrapUnwrap/types.js +0 -23
  78. package/dist/features/wrapUnwrap/uniqueNodeIdsPlugin.d.ts +0 -17
  79. package/dist/features/wrapUnwrap/uniqueNodeIdsPlugin.js +0 -106
  80. package/dist/listInputRules.d.ts +0 -2
  81. package/dist/listInputRules.js +0 -14
  82. package/dist/rebaseStep.d.ts +0 -9
  83. package/dist/rebaseStep.js +0 -11
  84. package/dist/testing/e2eTestSchema.d.ts +0 -2
  85. package/dist/transformToSuggestionTransaction.d.ts +0 -22
  86. package/dist/transformToSuggestionTransaction.js +0 -101
  87. package/dist/wrappingInputRule.d.ts +0 -4
  88. package/dist/wrappingInputRule.js +0 -28
@@ -1,15 +1,97 @@
1
+ import { AddMarkStep, AddNodeMarkStep, AttrStep, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep } from "prosemirror-transform";
2
+ import { trackAddMarkStep } from "./addMarkStep.js";
3
+ import { trackAddNodeMarkStep } from "./addNodeMarkStep.js";
4
+ import { trackAttrStep } from "./attrStep.js";
5
+ import { suggestRemoveMarkStep } from "./removeMarkStep.js";
6
+ import { suggestRemoveNodeMarkStep } from "./removeNodeMarkStep.js";
7
+ import { suggestReplaceAroundStep } from "./replaceAroundStep.js";
8
+ import { suggestReplaceStep } from "./replaceStep.js";
1
9
  import { isSuggestChangesEnabled, suggestChangesKey } from "./plugin.js";
10
+ import { generateNextNumberId } from "./generateId.js";
11
+ import { getSuggestionMarks } from "./utils.js";
2
12
  import { prependDeletionsWithZWSP } from "./prependDeletionsWithZWSP.js";
3
- import { getRequiredStructuralContextPaths, suggestStructureChanges } from "./features/wrapUnwrap/structureChangesPlugin.js";
4
- import { handleSpecialTransactionShape } from "./features/transactionShaping/index.js";
5
- import { transformToSuggestionTransaction } from "./transformToSuggestionTransaction.js";
6
- export { transformToSuggestionTransaction } from "./transformToSuggestionTransaction.js";
7
- export { suggestStructureChanges } from "./features/wrapUnwrap/structureChangesPlugin.js";
8
- const TRACE_ENABLED = false;
9
- function trace(...args) {
10
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
11
- if (!TRACE_ENABLED) return;
12
- console.log("[withSuggestChanges]", ...args);
13
+ function getStepHandler(step) {
14
+ if (step instanceof ReplaceStep) {
15
+ return suggestReplaceStep;
16
+ }
17
+ if (step instanceof ReplaceAroundStep) {
18
+ return suggestReplaceAroundStep;
19
+ }
20
+ if (step instanceof AddMarkStep) {
21
+ return trackAddMarkStep;
22
+ }
23
+ if (step instanceof RemoveMarkStep) {
24
+ return suggestRemoveMarkStep;
25
+ }
26
+ if (step instanceof AddNodeMarkStep) {
27
+ return trackAddNodeMarkStep;
28
+ }
29
+ if (step instanceof RemoveNodeMarkStep) {
30
+ return suggestRemoveNodeMarkStep;
31
+ }
32
+ if (step instanceof AttrStep) {
33
+ return trackAttrStep;
34
+ }
35
+ // Default handler — simply rebase the step onto the
36
+ // tracked transaction and apply it.
37
+ return (trackedTransaction, _state, _doc, step, prevSteps)=>{
38
+ const reset = prevSteps.slice().reverse().reduce((acc, step)=>acc?.map(step.getMap().invert()) ?? null, step);
39
+ const rebased = trackedTransaction.steps.reduce((acc, step)=>acc?.map(step.getMap()) ?? null, reset);
40
+ if (rebased) {
41
+ trackedTransaction.step(rebased);
42
+ }
43
+ return false;
44
+ };
45
+ }
46
+ /**
47
+ * Given a standard transaction from ProseMirror, produce
48
+ * a new transaction that tracks the changes from the original,
49
+ * rather than applying them.
50
+ *
51
+ * For each type of step, we implement custom behavior to prevent
52
+ * deletions from being removed from the document, instead adding
53
+ * deletion marks, and ensuring that all insertions have insertion
54
+ * marks.
55
+ */ export function transformToSuggestionTransaction(originalTransaction, state, generateId) {
56
+ getSuggestionMarks(state.schema);
57
+ let suggestionId = generateId ? generateId(state.schema, originalTransaction.docs[0]) : generateNextNumberId(state.schema, originalTransaction.docs[0]);
58
+ // Create a new transaction from scratch. The original transaction
59
+ // is going to be dropped in favor of this one.
60
+ const trackedTransaction = state.tr;
61
+ for(let i = 0; i < originalTransaction.steps.length; i++){
62
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
63
+ const step = originalTransaction.steps[i];
64
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
65
+ const doc = originalTransaction.docs[i];
66
+ const stepTracker = getStepHandler(step);
67
+ if (stepTracker(trackedTransaction, state, doc, step, originalTransaction.steps.slice(0, i), suggestionId) && i < originalTransaction.steps.length - 1) {
68
+ // If the suggestionId was used by one of the step handlers,
69
+ // increment it so that it's not reused.
70
+ if (generateId) {
71
+ suggestionId = generateId(state.schema, trackedTransaction.doc);
72
+ } else if (typeof suggestionId === "number") {
73
+ suggestionId = suggestionId + 1;
74
+ }
75
+ }
76
+ continue;
77
+ }
78
+ if (originalTransaction.selectionSet && !trackedTransaction.selectionSet) {
79
+ // Map the original selection backwards through the original transaction,
80
+ // and then forwards through the new one.
81
+ const originalBaseDoc = originalTransaction.docs[0];
82
+ const base = originalBaseDoc ? originalTransaction.selection.map(originalBaseDoc, originalTransaction.mapping.invert()) : originalTransaction.selection;
83
+ trackedTransaction.setSelection(base.map(trackedTransaction.doc, trackedTransaction.mapping));
84
+ }
85
+ if (originalTransaction.scrolledIntoView) {
86
+ trackedTransaction.scrollIntoView();
87
+ }
88
+ if (originalTransaction.storedMarksSet) {
89
+ trackedTransaction.setStoredMarks(originalTransaction.storedMarks);
90
+ }
91
+ // @ts-expect-error Preserve original transaction meta exactly as-is
92
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
93
+ trackedTransaction.meta = originalTransaction.meta;
94
+ return trackedTransaction;
13
95
  }
14
96
  /**
15
97
  * A `dispatchTransaction` decorator. Wrap your existing `dispatchTransaction`
@@ -21,65 +103,13 @@ function trace(...args) {
21
103
  * These modified transactions will suggest changes instead of directly
22
104
  * applying them, e.g. by marking a range with the deletion mark rather
23
105
  * than removing it from the document.
24
- */ export function withSuggestChanges(dispatchTransaction, generateId, opts) {
106
+ */ export function withSuggestChanges(dispatchTransaction, generateId) {
25
107
  const dispatch = dispatchTransaction ?? function(tr) {
26
108
  this.updateState(this.state.apply(tr));
27
109
  };
28
110
  return function dispatchTransaction(tr) {
29
111
  const ySyncMeta = tr.getMeta("y-sync$") ?? {};
30
- const isEnabled = isSuggestChangesEnabled(this.state) && !tr.getMeta("history$") && !tr.getMeta("collab$") && !ySyncMeta.isUndoRedoOperation && !ySyncMeta.isChangeOrigin && !("skip" in (tr.getMeta(suggestChangesKey) ?? {}));
31
- let transaction = tr;
32
- if (isEnabled) {
33
- let structureChangesResult = null;
34
- const docBefore = transaction.docs[0];
35
- const structuralContextPaths = opts?.experimental_trackStructureChanges ? getRequiredStructuralContextPaths(opts.experimental_trackStructures) : null;
36
- const ensureUniqueNodeIds = opts?.experimental_ensureUniqueNodeIds;
37
- const shapedTransaction = transaction.docChanged ? handleSpecialTransactionShape({
38
- transaction,
39
- state: this.state,
40
- generateId,
41
- structuralContextPaths,
42
- ensureUniqueNodeIds
43
- }) : null;
44
- if (!shapedTransaction && transaction.docChanged && docBefore && structuralContextPaths && typeof ensureUniqueNodeIds === "function") {
45
- trace("trying to track structure changes first...");
46
- // after a transaction, some nodes may not yet have unique ids (they were just added, and the unique id plugin has not yet run)
47
- // this hook allows to "post-process" the transaction and add the missing ids
48
- // basically it allows to run the core logic of the unique ids plugin earlier
49
- const perfUid = performance.now();
50
- const uniqueNodeIdsTransform = ensureUniqueNodeIds([
51
- transaction
52
- ], docBefore, transaction.doc);
53
- trace("perf", "structure", "ensureUniqueNodsIds took", Number((performance.now() - perfUid).toFixed(2)), "ms");
54
- const docAfter = uniqueNodeIdsTransform.doc;
55
- trace("unique node ids set", docAfter);
56
- // try running structure changes first
57
- // if handled, then ignore the main plugin
58
- // otherwise use the main plugin
59
- const perfStructure = performance.now();
60
- structureChangesResult = suggestStructureChanges(docBefore, docAfter, structuralContextPaths, generateId);
61
- trace("perf", "structure", "suggestStructureChanges took", Number((performance.now() - perfStructure).toFixed(2)), "ms");
62
- trace("structure changes transform completed", structureChangesResult.transform);
63
- if (structureChangesResult.handled) {
64
- uniqueNodeIdsTransform.steps.forEach((step)=>{
65
- transaction.step(step);
66
- });
67
- structureChangesResult.transform.steps.forEach((step)=>{
68
- transaction.step(step);
69
- });
70
- trace("applied unique id transform and structure changes transform to the transaction", transaction);
71
- }
72
- }
73
- if (shapedTransaction) {
74
- transaction = shapedTransaction;
75
- } else if (transaction.docChanged && structureChangesResult?.handled !== true) {
76
- trace("running the main suggestions plugin...");
77
- const perfSuggestions = performance.now();
78
- transaction = transformToSuggestionTransaction(tr, this.state, generateId);
79
- trace("perf", "suggestions", "transformToSuggestionTransaction took", Number((performance.now() - perfSuggestions).toFixed(2)), "ms");
80
- trace("main suggestions plugin completed", transaction);
81
- }
82
- }
112
+ const transaction = isSuggestChangesEnabled(this.state) && !tr.getMeta("history$") && !tr.getMeta("collab$") && !ySyncMeta.isUndoRedoOperation && !ySyncMeta.isChangeOrigin && !("skip" in (tr.getMeta(suggestChangesKey) ?? {})) ? transformToSuggestionTransaction(tr, this.state, generateId) : tr;
83
113
  if (transaction.docChanged) {
84
114
  prependDeletionsWithZWSP(transaction);
85
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-marker/prosemirror-suggest-changes",
3
- "version": "0.3.3-wrap-unwrap.29",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",
@@ -0,0 +1,8 @@
1
+ When nodes are joined using backspace or delete, let the join happen normally,
2
+ but mark former node boundaries with deletion mark of type="join".
3
+
4
+ Save information about what nodes were at each side of the boundary before the
5
+ join.
6
+
7
+ Then on restore, use marks at join points to split the node and restore the node
8
+ markup at each side of the split.
@@ -1,6 +0,0 @@
1
- import { type JoinMarkAttrs } from "./types.js";
2
- export declare const MAX_BLOCK_JOIN_DEPTH = 2;
3
- export declare function normalizeJoinNodesMetadata(attrs: JoinMarkAttrs): false | {
4
- leftNodes: import("./types.js").SerializedJoinNode[];
5
- rightNodes: import("./types.js").SerializedJoinNode[];
6
- };
@@ -1,24 +0,0 @@
1
- import { isSerializedJoinNode } from "./types.js";
2
- // Block join suggestion metadata/revert currently supports the depth TipTap uses
3
- // when Backspace joins both list-item paragraphs and their parent list items.
4
- export const MAX_BLOCK_JOIN_DEPTH = 2;
5
- export function normalizeJoinNodesMetadata(attrs) {
6
- const data = attrs.data;
7
- // Normalize legacy metadata so revert can use the same array path.
8
- const leftNodes = data.leftNodes ?? (data.leftNode ? [
9
- data.leftNode
10
- ] : null);
11
- const rightNodes = data.rightNodes ?? (data.rightNode ? [
12
- data.rightNode
13
- ] : null);
14
- if (!Array.isArray(leftNodes) || !Array.isArray(rightNodes)) return false;
15
- if (leftNodes.length === 0 || leftNodes.length !== rightNodes.length) return false;
16
- // Reject unsupported depths instead of partially reverting unknown structure.
17
- if (leftNodes.length > MAX_BLOCK_JOIN_DEPTH) return false;
18
- if (!leftNodes.every(isSerializedJoinNode)) return false;
19
- if (!rightNodes.every(isSerializedJoinNode)) return false;
20
- return {
21
- leftNodes,
22
- rightNodes
23
- };
24
- }
@@ -1,34 +0,0 @@
1
- import { type Mark, type Attrs, type Node } from "prosemirror-model";
2
- export interface SerializedJoinNode {
3
- type: string;
4
- attrs: object;
5
- marks: {
6
- attrs: Record<string, unknown>;
7
- }[];
8
- }
9
- export interface JoinMarkAttrs {
10
- type: "join";
11
- data: {
12
- leftNode?: SerializedJoinNode;
13
- rightNode?: SerializedJoinNode;
14
- leftNodes?: SerializedJoinNode[];
15
- rightNodes?: SerializedJoinNode[];
16
- };
17
- }
18
- export interface JoinPair {
19
- leftNode: Node;
20
- rightNode: Node;
21
- }
22
- export interface JoinCandidate {
23
- joinPos: number;
24
- leftNodes: Node[];
25
- rightNodes: Node[];
26
- }
27
- export declare function isSerializedJoinNode(node: unknown): node is SerializedJoinNode;
28
- export declare function isJoinMarkAttrs(attrs: Attrs): attrs is JoinMarkAttrs;
29
- export declare function isJoinMarkObject(mark: unknown): mark is Omit<Mark, "attrs"> & {
30
- attrs: JoinMarkAttrs;
31
- };
32
- export declare function isJoinMark(mark: Mark): mark is Omit<Mark, "attrs"> & {
33
- attrs: JoinMarkAttrs;
34
- };
@@ -1,20 +0,0 @@
1
- import { getSuggestionMarks } from "../../utils.js";
2
- export function isSerializedJoinNode(node) {
3
- if (node === null || typeof node !== "object") return false;
4
- const data = node;
5
- return typeof data.type === "string" && typeof data.attrs === "object" && data.attrs !== null && Array.isArray(data.marks);
6
- }
7
- export function isJoinMarkAttrs(attrs) {
8
- if (attrs["type"] !== "join") return false;
9
- if (attrs["data"] == null) return false;
10
- return true;
11
- }
12
- export function isJoinMarkObject(mark) {
13
- if (mark === null || typeof mark !== "object") return false;
14
- if (!("attrs" in mark)) return false;
15
- return isJoinMarkAttrs(mark.attrs);
16
- }
17
- export function isJoinMark(mark) {
18
- const { deletion } = getSuggestionMarks(mark.type.schema);
19
- return mark.type === deletion && isJoinMarkAttrs(mark.attrs);
20
- }
@@ -1,3 +0,0 @@
1
- import { type Transaction } from "prosemirror-state";
2
- import { type SpecialTransactionShape } from "./types.js";
3
- export declare function detectSpecialTransactionShape(transaction: Transaction): SpecialTransactionShape | null;
@@ -1,4 +0,0 @@
1
- import { detectTipTapParagraphIntoListJoin } from "./tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.js";
2
- export function detectSpecialTransactionShape(transaction) {
3
- return detectTipTapParagraphIntoListJoin(transaction);
4
- }
@@ -1,3 +0,0 @@
1
- import { type Transaction } from "prosemirror-state";
2
- import { type HandleSpecialTransactionShapeArgs } from "./types.js";
3
- export declare function handleSpecialTransactionShape(args: HandleSpecialTransactionShapeArgs): Transaction | null;
@@ -1,11 +0,0 @@
1
- import { handleTipTapParagraphIntoListJoin } from "./tipTapParagraphIntoListJoin/index.js";
2
- const specialTransactionShapeHandlers = [
3
- handleTipTapParagraphIntoListJoin
4
- ];
5
- export function handleSpecialTransactionShape(args) {
6
- for (const handler of specialTransactionShapeHandlers){
7
- const transaction = handler(args);
8
- if (transaction) return transaction;
9
- }
10
- return null;
11
- }
@@ -1,3 +0,0 @@
1
- import { type Transaction } from "prosemirror-state";
2
- import { type TipTapParagraphIntoListJoinShape } from "../types.js";
3
- export declare function detectTipTapParagraphIntoListJoin(transaction: Transaction): TipTapParagraphIntoListJoinShape | null;
@@ -1,48 +0,0 @@
1
- import { ReplaceStep, Transform } from "prosemirror-transform";
2
- import { getNodeId } from "../../wrapUnwrap/getNodeId.js";
3
- // detect a very specific TipTap pattern where joining a paragraph into a list above it
4
- // causes a 3-step transaction, which is different from normal prosemirror where you just get 1 step
5
- // see unit test for details
6
- export function detectTipTapParagraphIntoListJoin(transaction) {
7
- if (transaction.steps.length !== 3) return null;
8
- const [deleteStep, insertStep, joinStep] = transaction.steps;
9
- if (!(deleteStep instanceof ReplaceStep) || !(insertStep instanceof ReplaceStep) || !(joinStep instanceof ReplaceStep)) {
10
- return null;
11
- }
12
- if (deleteStep.slice.content.size !== 0) return null;
13
- if (insertStep.from !== insertStep.to) return null;
14
- if (insertStep.slice.openStart !== 0 || insertStep.slice.openEnd !== 0) return null;
15
- if (insertStep.slice.content.childCount !== 1) return null;
16
- if (joinStep.slice.content.size !== 0) return null;
17
- if (joinStep.structure !== true) return null;
18
- const docBefore = transaction.docs[0];
19
- if (!docBefore) return null;
20
- const movedNode = docBefore.nodeAt(deleteStep.from);
21
- if (!movedNode || !movedNode.isTextblock || movedNode.nodeSize !== deleteStep.to - deleteStep.from) {
22
- return null;
23
- }
24
- const insertedNode = insertStep.slice.content.firstChild;
25
- if (!insertedNode?.isTextblock) return null;
26
- if (insertedNode.type !== movedNode.type) return null;
27
- if (insertedNode.textContent !== movedNode.textContent) return null;
28
- const movedNodeId = getNodeId(movedNode);
29
- if (!movedNodeId || getNodeId(insertedNode) !== movedNodeId) return null;
30
- const previousSibling = docBefore.resolve(deleteStep.from).nodeBefore;
31
- if (!previousSibling || previousSibling.isInline) return null;
32
- try {
33
- const preview = new Transform(docBefore);
34
- preview.step(deleteStep);
35
- preview.step(insertStep);
36
- new Transform(preview.doc).step(joinStep);
37
- } catch {
38
- return null;
39
- }
40
- console.warn("[prosemirror-suggest-changes]", "detected TipTap paragraph into list join shape");
41
- return {
42
- type: "tipTapParagraphIntoListJoin",
43
- deleteStep,
44
- insertStep,
45
- joinStep,
46
- movedNode
47
- };
48
- }
@@ -1,188 +0,0 @@
1
- import { EditorState } from "prosemirror-state";
2
- import { Step } from "prosemirror-transform";
3
- import { describe, expect, it } from "vitest";
4
- import { createSchema } from "../../../testing/e2eTestSchema.js";
5
- import { detectTipTapParagraphIntoListJoin } from "./detectTipTapParagraphIntoListJoin.js";
6
- const schema = createSchema();
7
- // when joining a paragraph into a list above it,
8
- // TipTap List extension overrides the default ProseMirror behavior
9
- // by default, ProseMirror puts the paragraph to the end of list as a separate list item
10
- // TipTap instead joins the paragraph with the paragraph of the last list item
11
- const TIPTAP_PARAGRAPH_INTO_LIST_STEPS = [
12
- // first step deletes the paragraph that we're joining
13
- {
14
- stepType: "replace",
15
- from: 42,
16
- to: 60
17
- },
18
- // second step inserts that paragraph into the last list item
19
- {
20
- stepType: "replace",
21
- from: 40,
22
- to: 40,
23
- slice: {
24
- content: [
25
- {
26
- type: "paragraph",
27
- attrs: {
28
- id: "node-9",
29
- textAlign: null
30
- },
31
- content: [
32
- {
33
- type: "text",
34
- text: "sample paragraph"
35
- }
36
- ]
37
- }
38
- ]
39
- }
40
- },
41
- // third step joins the two paragraphs in the list item
42
- {
43
- stepType: "replace",
44
- from: 39,
45
- to: 41,
46
- structure: true
47
- }
48
- ];
49
- const TIPTAP_PARAGRAPH_INTO_LIST_DOC = {
50
- type: "doc",
51
- content: [
52
- {
53
- type: "orderedList",
54
- attrs: {
55
- order: 1,
56
- id: "node-0"
57
- },
58
- content: [
59
- {
60
- type: "listItem",
61
- attrs: {
62
- id: "node-1"
63
- },
64
- content: [
65
- {
66
- type: "paragraph",
67
- attrs: {
68
- id: "node-2"
69
- },
70
- content: [
71
- {
72
- type: "text",
73
- text: "Item 1"
74
- }
75
- ]
76
- }
77
- ]
78
- },
79
- {
80
- type: "listItem",
81
- attrs: {
82
- id: "node-3"
83
- },
84
- content: [
85
- {
86
- type: "paragraph",
87
- attrs: {
88
- id: "node-4"
89
- },
90
- content: [
91
- {
92
- type: "text",
93
- text: "Item 2"
94
- }
95
- ]
96
- }
97
- ]
98
- },
99
- {
100
- type: "listItem",
101
- attrs: {
102
- id: "node-5"
103
- },
104
- content: [
105
- {
106
- type: "paragraph",
107
- attrs: {
108
- id: "node-6"
109
- },
110
- content: [
111
- {
112
- type: "text",
113
- text: "Item 3"
114
- }
115
- ]
116
- }
117
- ]
118
- },
119
- {
120
- type: "listItem",
121
- attrs: {
122
- id: "node-7"
123
- },
124
- content: [
125
- {
126
- type: "paragraph",
127
- attrs: {
128
- id: "node-8"
129
- },
130
- content: [
131
- {
132
- type: "text",
133
- text: "Item 4"
134
- }
135
- ]
136
- }
137
- ]
138
- }
139
- ]
140
- },
141
- {
142
- type: "paragraph",
143
- attrs: {
144
- id: "node-9"
145
- },
146
- content: [
147
- {
148
- type: "text",
149
- text: "sample paragraph"
150
- }
151
- ]
152
- }
153
- ]
154
- };
155
- describe("detectTipTapParagraphIntoListJoin", ()=>{
156
- it("detects TipTap's three-step paragraph-into-list join shape", ()=>{
157
- const doc = schema.nodeFromJSON(TIPTAP_PARAGRAPH_INTO_LIST_DOC);
158
- const state = EditorState.create({
159
- doc
160
- });
161
- const transaction = state.tr;
162
- TIPTAP_PARAGRAPH_INTO_LIST_STEPS.forEach((stepJSON)=>{
163
- transaction.step(Step.fromJSON(schema, stepJSON));
164
- });
165
- const shape = detectTipTapParagraphIntoListJoin(transaction);
166
- expect(shape).toMatchObject({
167
- type: "tipTapParagraphIntoListJoin",
168
- deleteStep: {
169
- from: 42,
170
- to: 60
171
- },
172
- insertStep: {
173
- from: 40,
174
- to: 40
175
- },
176
- joinStep: {
177
- from: 39,
178
- to: 41
179
- },
180
- movedNode: {
181
- attrs: {
182
- id: "node-9"
183
- }
184
- }
185
- });
186
- expect(shape?.movedNode.textContent).toBe("sample paragraph");
187
- });
188
- });
@@ -1,3 +0,0 @@
1
- import { type Transaction } from "prosemirror-state";
2
- import { type HandleSpecialTransactionShapeArgs } from "../types.js";
3
- export declare function handleTipTapParagraphIntoListJoin(args: HandleSpecialTransactionShapeArgs): Transaction | null;
@@ -1,64 +0,0 @@
1
- import { EditorState } from "prosemirror-state";
2
- import { preserveTransactionData, transformToSuggestionTransaction } from "../../../transformToSuggestionTransaction.js";
3
- import { generateNextNumberId } from "../../../generateId.js";
4
- import { suggestStructureChanges } from "../../wrapUnwrap/structureChangesPlugin.js";
5
- import { detectSpecialTransactionShape } from "../detectSpecialTransactionShape.js";
6
- // handle the specific TipTap pattern when backspacing from a paragraph into a list above
7
- // causes a transaction with 3 steps
8
- // "slice" the transaction into two pieces, first piece is the first 2 steps, second piece is the last step
9
- // first piece "moves" the paragraph - handled by structure change tracking - a structure "move" suggestion
10
- // second piece joins two paragraphs - handled by the main plugin join-on-delete feature - deletion type="join" mark
11
- export function handleTipTapParagraphIntoListJoin(args) {
12
- const shape = detectSpecialTransactionShape(args.transaction);
13
- if (!isTipTapParagraphIntoListJoinShape(shape)) return null;
14
- if (!args.structuralContextPaths || !args.ensureUniqueNodeIds) return null;
15
- const docBefore = args.transaction.docs[0];
16
- if (!docBefore) return null;
17
- const trackedTransaction = args.state.tr;
18
- const sharedSuggestionId = args.generateId ? args.generateId(args.state.schema, docBefore) : generateNextNumberId(args.state.schema, docBefore);
19
- const generateSharedSuggestionId = ()=>sharedSuggestionId;
20
- try {
21
- trackedTransaction.step(shape.deleteStep);
22
- trackedTransaction.step(shape.insertStep);
23
- } catch {
24
- return null;
25
- }
26
- const uniqueNodeIdsTransform = args.ensureUniqueNodeIds([
27
- args.transaction
28
- ], docBefore, trackedTransaction.doc);
29
- uniqueNodeIdsTransform.steps.forEach((step)=>{
30
- trackedTransaction.step(step);
31
- });
32
- const structureChangesResult = suggestStructureChanges(docBefore, uniqueNodeIdsTransform.doc, args.structuralContextPaths, generateSharedSuggestionId);
33
- if (!structureChangesResult.handled) {
34
- return null;
35
- }
36
- structureChangesResult.transform.steps.forEach((step)=>{
37
- trackedTransaction.step(step);
38
- });
39
- const intermediateState = EditorState.create({
40
- schema: args.state.schema,
41
- doc: trackedTransaction.doc
42
- });
43
- const joinTransaction = intermediateState.tr;
44
- try {
45
- joinTransaction.step(shape.joinStep);
46
- } catch {
47
- return null;
48
- }
49
- const trackedJoinTransaction = transformToSuggestionTransaction(joinTransaction, intermediateState, generateSharedSuggestionId);
50
- trackedJoinTransaction.steps.forEach((step)=>{
51
- trackedTransaction.step(step);
52
- });
53
- preserveTransactionData(trackedTransaction, trackedJoinTransaction, {
54
- selection: "currentDocument",
55
- preserveScroll: false,
56
- preserveStoredMarks: false,
57
- preserveMeta: false
58
- });
59
- preserveTransactionData(trackedTransaction, args.transaction);
60
- return trackedTransaction;
61
- }
62
- function isTipTapParagraphIntoListJoinShape(shape) {
63
- return shape?.type === "tipTapParagraphIntoListJoin";
64
- }
@@ -1,2 +0,0 @@
1
- export { detectTipTapParagraphIntoListJoin } from "./detectTipTapParagraphIntoListJoin.js";
2
- export { handleTipTapParagraphIntoListJoin } from "./handleTipTapParagraphIntoListJoin.js";
@@ -1,2 +0,0 @@
1
- export { detectTipTapParagraphIntoListJoin } from "./detectTipTapParagraphIntoListJoin.js";
2
- export { handleTipTapParagraphIntoListJoin } from "./handleTipTapParagraphIntoListJoin.js";