Skip to content

Commit 72689cd

Browse files
committed
fix: fix issue with moved cleanUpListsWithAnnotations, move both clean up functions
1 parent 0bb9c3b commit 72689cd

5 files changed

Lines changed: 146 additions & 121 deletions

File tree

packages/super-editor/src/core/helpers/annotator.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { Fragment } from 'prosemirror-model';
2-
import { fieldAnnotationHelpers } from '@extensions/index.js';
32
import { createHeaderFooterEditor, onHeaderFooterDataUpdate } from '@extensions/pagination/pagination-helpers.js';
4-
import { findParentNodeClosestToPos } from './findParentNodeClosestToPos';
53

64
/**
75
* Get the field attributes based on the field type and value
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { getAllFieldAnnotations } from '../fieldAnnotationHelpers/index.js';
2+
import { findParentNodeClosestToPos } from '@core/helpers/index.js';
3+
4+
/**
5+
* Clean up lists that contain field annotations if there are annotations
6+
* being deleted.
7+
* @param {string[]} fieldsToDelete - Array of field IDs to delete.
8+
* @returns {function} A ProseMirror command function.
9+
*/
10+
export const cleanUpListsWithAnnotations =
11+
(fieldsToDelete = []) =>
12+
({ dispatch, tr, state }) => {
13+
if (!dispatch) return true;
14+
15+
if (!Array.isArray(fieldsToDelete)) fieldsToDelete = [fieldsToDelete];
16+
const { doc } = state;
17+
const docxAnnotations = getAllFieldAnnotations(state) || [];
18+
19+
const nodesToDelete = [];
20+
21+
fieldsToDelete.forEach((fieldId) => {
22+
const matched = docxAnnotations.find((a) => a.node.attrs.fieldId === fieldId);
23+
if (!matched) return;
24+
25+
// find the nearest listItem
26+
const listItem = findParentNodeClosestToPos(doc.resolve(matched.pos), (node) => node.type.name === 'listItem');
27+
if (!listItem) return;
28+
29+
let remainingNodes = 0;
30+
listItem.node.descendants((node) => {
31+
if (node.type.name === 'fieldAnnotation') {
32+
remainingNodes += 1;
33+
}
34+
});
35+
36+
let matchingNodesFound = 0;
37+
let hasOtherNodes = false;
38+
listItem.node.children.forEach((child, index) => {
39+
const { type } = child;
40+
if (type.name !== 'paragraph' && type.name !== 'fieldAnnotation') return;
41+
42+
child.children.forEach((inline) => {
43+
const isFieldToDelete = fieldsToDelete.includes(inline.attrs.fieldId);
44+
const isFieldType = inline.type.name === 'fieldAnnotation';
45+
const isMatchingField = isFieldType && isFieldToDelete;
46+
if (!isFieldType && !isMatchingField) hasOtherNodes = true;
47+
if (isMatchingField) matchingNodesFound += 1;
48+
});
49+
});
50+
51+
if (!hasOtherNodes && matchingNodesFound > 0) {
52+
remainingNodes -= matchingNodesFound;
53+
}
54+
55+
if (remainingNodes > 0) {
56+
return;
57+
}
58+
59+
// now “bubble up” as long as each parent has exactly one child
60+
let { pos, node, depth } = listItem;
61+
let $pos = doc.resolve(pos);
62+
63+
while (depth > 0) {
64+
const parent = $pos.node(depth - 1);
65+
if (parent.childCount === 1) {
66+
// climb one level
67+
depth -= 1;
68+
pos = $pos.before(depth);
69+
node = parent;
70+
$pos = doc.resolve(pos);
71+
} else {
72+
break;
73+
}
74+
}
75+
76+
// dedupe
77+
if (!nodesToDelete.some((n) => n.pos === pos)) {
78+
nodesToDelete.push({ pos, node });
79+
}
80+
});
81+
82+
if (!nodesToDelete.length) return true;
83+
84+
// delete from back to front
85+
nodesToDelete
86+
.sort((a, b) => b.pos - a.pos)
87+
.forEach(({ pos, node }) => {
88+
tr.delete(pos, pos + node.nodeSize);
89+
});
90+
91+
return true;
92+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { findFieldAnnotationsByFieldId } from '../fieldAnnotationHelpers/index.js';
2+
3+
/**
4+
* Clean up paragraphs that contain field annotations marked for deletion.
5+
* If a paragraph has only one field annotation and no other content,
6+
* it will be deleted.
7+
* @param {string[]} fieldsToDelete - Array of field IDs to delete.
8+
* @returns {function} A ProseMirror command function.
9+
*/
10+
export const cleanUpParagraphWithAnnotations =
11+
(fieldsToDelete = []) =>
12+
({ dispatch, tr, state }) => {
13+
if (!dispatch) return true;
14+
const annotations = findFieldAnnotationsByFieldId(fieldsToDelete, state) || [];
15+
const nodesToDelete = [];
16+
const { doc } = state;
17+
18+
annotations.forEach((annotation) => {
19+
let { pos, node } = annotation;
20+
let newPosFrom = tr.mapping.map(pos);
21+
22+
const resolvedPos = doc.resolve(newPosFrom);
23+
const parent = resolvedPos.parent;
24+
25+
let currentNode = tr.doc.nodeAt(newPosFrom);
26+
if (node.eq(currentNode) && parent?.children?.length < 2) {
27+
nodesToDelete.push({ pos: newPosFrom, node: parent });
28+
}
29+
});
30+
31+
if (!nodesToDelete.length) return true;
32+
33+
nodesToDelete
34+
.sort((a, b) => b.pos - a.pos)
35+
.forEach(({ pos, node }) => {
36+
tr.delete(pos, pos + node.nodeSize);
37+
});
38+
39+
return true;
40+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as cleanUpListsCommands from './cleanUpListsWithAnnotations.js';
2+
import * as cleanUpParagraphCommands from './cleanUpParagraphWithAnnotations.js';
3+
4+
export const commands = {
5+
...cleanUpListsCommands,
6+
...cleanUpParagraphCommands,
7+
};
8+
9+
export * from './cleanUpListsWithAnnotations.js';
10+
export * from './cleanUpParagraphWithAnnotations.js';

packages/super-editor/src/extensions/field-annotation/field-annotation.js

Lines changed: 4 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { toHex } from 'color2k';
1010
import { parseSizeUnit, minMax } from '@core/utilities/index.js';
1111
import { NodeSelection, Selection } from 'prosemirror-state';
1212
import { generateDocxRandomId } from '../../core/helpers/index.js';
13+
import { commands as cleanupCommands } from './cleanup-commands/index.js';
1314

1415
export const fieldAnnotationName = 'fieldAnnotation';
1516
export const annotationClass = 'annotation';
@@ -1114,128 +1115,12 @@ export const FieldAnnotation = Node.create({
11141115
}
11151116
}
11161117

1117-
return true;
1118-
},
1119-
1120-
cleanUpListsWithAnnotations:
1121-
(fieldsToDelete = []) =>
1122-
({ dispatch, tr, state }) => {
1123-
if (!dispatch) return true;
1124-
1125-
if (!Array.isArray(fieldsToDelete)) fieldsToDelete = [fieldsToDelete];
1126-
const { doc } = state;
1127-
const docxAnnotations = getAllFieldAnnotations(state) || [];
1128-
1129-
const nodesToDelete = [];
1130-
1131-
fieldsToDelete.forEach((fieldId) => {
1132-
const matched = docxAnnotations.find(a => a.node.attrs.fieldId === fieldId);
1133-
if (!matched) return;
1134-
1135-
// find the nearest listItem
1136-
const listItem = findParentNodeClosestToPos(
1137-
doc.resolve(matched.pos),
1138-
node => node.type.name === 'listItem'
1139-
);
1140-
if (!listItem) return;
1141-
1142-
let remainingNodes = 0;
1143-
listItem.node.descendants((node) => {
1144-
if (node.type.name === 'fieldAnnotation') {
1145-
remainingNodes += 1;
1146-
}
1147-
})
1148-
1149-
let matchingNodesFound = 0;
1150-
let hasOtherNodes = false;
1151-
listItem.node.children.forEach((child, index) => {
1152-
const { type } = child;
1153-
if (type.name !== 'paragraph' && type.name !== 'fieldAnnotation') return;
1154-
1155-
child.children.forEach((inline) => {
1156-
const isFieldToDelete = fieldsToDelete.includes(inline.attrs.fieldId);
1157-
const isFieldType = inline.type.name === 'fieldAnnotation';
1158-
const isMatchingField = isFieldType && isFieldToDelete;
1159-
if (!isFieldType && !isMatchingField) hasOtherNodes = true;
1160-
if (isMatchingField) matchingNodesFound += 1;
1161-
});
1162-
});
1163-
1164-
if (!hasOtherNodes && matchingNodesFound > 0) {
1165-
remainingNodes -= matchingNodesFound;
1166-
}
1167-
1168-
if (remainingNodes > 0) {
1169-
return;
1170-
}
1171-
1172-
// now “bubble up” as long as each parent has exactly one child
1173-
let { pos, node, depth } = listItem;
1174-
let $pos = doc.resolve(pos);
1175-
1176-
while (depth > 0) {
1177-
const parent = $pos.node(depth - 1);
1178-
if (parent.childCount === 1) {
1179-
// climb one level
1180-
depth -= 1;
1181-
pos = $pos.before(depth);
1182-
node = parent;
1183-
$pos = doc.resolve(pos);
1184-
} else {
1185-
break;
1186-
}
1187-
}
1188-
1189-
// dedupe
1190-
if (!nodesToDelete.some(n => n.pos === pos)) {
1191-
nodesToDelete.push({ pos, node });
1192-
}
1193-
});
1194-
1195-
if (!nodesToDelete.length) return true;
1196-
1197-
// delete from back to front
1198-
nodesToDelete
1199-
.sort((a, b) => b.pos - a.pos)
1200-
.forEach(({ pos, node }) => {
1201-
tr.delete(pos, pos + node.nodeSize);
1202-
});
1203-
1204-
return true;
1205-
},
1206-
1207-
cleanUpParagraphWithAnnotations:
1208-
(fieldsToDelete = []) =>
1209-
({ dispatch, tr, state }) => {
1210-
if (!dispatch) return true;
1211-
const annotations = findFieldAnnotationsByFieldId(fieldsToDelete, state) || [];
1212-
const nodesToDelete = [];
1213-
const { doc } = state;
1214-
1215-
annotations.forEach((annotation) => {
1216-
let { pos, node } = annotation;
1217-
let newPosFrom = tr.mapping.map(pos);
1218-
1219-
const resolvedPos = doc.resolve(newPosFrom);
1220-
const parent = resolvedPos.parent;
1221-
1222-
let currentNode = tr.doc.nodeAt(newPosFrom);
1223-
if (node.eq(currentNode) && parent?.children?.length < 2) {
1224-
nodesToDelete.push({ pos: newPosFrom, node: parent });
1225-
}
1226-
});
1227-
1228-
if (!nodesToDelete.length) return true;
1229-
1230-
nodesToDelete
1231-
.sort((a, b) => b.pos - a.pos)
1232-
.forEach(({ pos, node }) => {
1233-
tr.delete(pos, pos + node.nodeSize);
1234-
});
1235-
12361118
return true;
12371119
},
12381120
/// Formatting commands - end.
1121+
1122+
// Clean up commands (after field deletion)
1123+
...cleanupCommands,
12391124
};
12401125
},
12411126

0 commit comments

Comments
 (0)