Skip to content

Commit 1708ca1

Browse files
committed
Update table generation logic to handle copied annotations, improved row generation, more robust handling
1 parent 53ebce6 commit 1708ca1

5 files changed

Lines changed: 140 additions & 70 deletions

File tree

packages/super-editor/src/core/Editor.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { initPaginationData, PaginationPluginKey } from '@extensions/pagination/
1717
import { CommentsPluginKey } from '@extensions/comment/comments-plugin.js';
1818
import { getNecessaryMigrations } from '@core/migrations/index.js';
1919
import { getRichTextExtensions } from '../extensions/index.js';
20+
import { AnnotatorServices } from '@helpers/annotator.js';
2021
import {
2122
prepareCommentsForExport,
2223
prepareCommentsForImport,
@@ -1207,4 +1208,16 @@ export class Editor extends EventEmitter {
12071208
dispatch(tr);
12081209
}
12091210

1211+
/**
1212+
* A command to prepare the editor to receive annotations. This will
1213+
* pre-process the document as needed prior to running in the annotator.
1214+
*
1215+
* Currently this is only used for table generation but additional pre-processing can be done here.
1216+
* @param {Array[Object]} annotationValues
1217+
* @returns {void}
1218+
*/
1219+
prepareForAnnotations(annotationValues = []) {
1220+
AnnotatorServices.processTables({ editor: this, annotationValues });
1221+
}
1222+
12101223
}

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Fragment } from 'prosemirror-model';
2+
13
/**
24
* Get the field attributes based on the field type and value
35
*
@@ -25,3 +27,117 @@ const annotateHtml = (value) => ({ rawHtml: value });
2527
const annotateText = (value) => ({ displayLabel: value });
2628
const annotateImage = (value) => ({ imageSrc: value });
2729
const annotateCheckbox = (value) => ({ displayLabel: value });
30+
31+
/**
32+
* Pre-process tables in the document to generate rows from annotations if necessary
33+
*
34+
* @param {Object} param0 The editor instance and annotation values
35+
* @param {Object} param0.editor The editor instance
36+
* @param {Array} param0.annotationValues The annotation values to process
37+
*/
38+
export const processTables = ({ editor, annotationValues }) => {
39+
const { state } = editor;
40+
const { doc } = state;
41+
const { tr } = state;
42+
const { dispatch } = editor.view;
43+
44+
// Get all tables in the document
45+
const tables = [];
46+
doc.descendants((node, pos) => {
47+
if (node.type.name === 'table') tables.push({ node, pos });
48+
});
49+
50+
tables.reverse().forEach((table) => {
51+
generateTableIfNecessary({ tableNode: table, annotationValues, tr, editor });
52+
});
53+
dispatch(tr);
54+
};
55+
const generateTableIfNecessary = ({ tableNode, annotationValues, tr, editor }) => {
56+
let rowNodeToGenerate = null;
57+
let currentRow = null;
58+
59+
const {
60+
tableRow: RowType,
61+
tableCell: CellType,
62+
fieldAnnotation: FieldType,
63+
paragraph: ParaType
64+
} = editor.schema.nodes;
65+
66+
// Find the row with fieldAnnotations that are arrays
67+
tableNode.node.descendants((node, pos) => {
68+
if (rowNodeToGenerate) return true;
69+
if (node.type === RowType) currentRow = { node, pos };
70+
if (node.type === FieldType) {
71+
const annotationValue = getAnnotationValue(node.attrs.fieldId, annotationValues);
72+
if (Array.isArray(annotationValue)) rowNodeToGenerate = currentRow;
73+
}
74+
});
75+
76+
if (!rowNodeToGenerate) return;
77+
78+
const { node: rowNode, pos: rowStartPos } = rowNodeToGenerate;
79+
const absoluteRowStart = tr.mapping.map(tableNode.pos + rowStartPos);
80+
const absoluteRowEnd = absoluteRowStart + rowNode.nodeSize;
81+
82+
const rowAnnotations = [];
83+
let rowsToGenerate = 0;
84+
rowNode.descendants((childNode, childPos) => {
85+
if (childNode.type === FieldType) {
86+
const annotationValue = getAnnotationValue(childNode.attrs.fieldId, annotationValues);
87+
rowAnnotations.push({ node: childNode, pos: childPos, values: annotationValue });
88+
if (Array.isArray(annotationValue)) {
89+
rowsToGenerate = Math.max(rowsToGenerate, annotationValue.length);
90+
}
91+
}
92+
});
93+
94+
if (rowsToGenerate <= 1) return;
95+
96+
const rebuildCell = (cellNode, rowIndex) => {
97+
const updatedBlocks = cellNode.content.content.map((blockNode) => {
98+
if (blockNode.type !== ParaType) return blockNode;
99+
100+
const updatedInlines = blockNode.content.content.map((inlineNode) => {
101+
if (inlineNode.type !== FieldType) return inlineNode;
102+
103+
let matchedAnnotationValues = getAnnotationValue(inlineNode.attrs.fieldId, annotationValues);
104+
if (!Array.isArray(matchedAnnotationValues)) matchedAnnotationValues = [matchedAnnotationValues];
105+
const value = matchedAnnotationValues?.[rowIndex];
106+
107+
const extraAttrs = getFieldAttrs(inlineNode, value);
108+
return FieldType.create(
109+
{ ...inlineNode.attrs, ...extraAttrs, generatorIndex: rowIndex },
110+
inlineNode.content,
111+
inlineNode.marks
112+
);
113+
});
114+
115+
return ParaType.create(blockNode.attrs, Fragment.from(updatedInlines), blockNode.marks);
116+
});
117+
118+
return CellType.create(cellNode.attrs, Fragment.from(updatedBlocks), cellNode.marks);
119+
};
120+
121+
// Insert new rows in reverse *after* the current row
122+
for (let rowIndex = rowsToGenerate - 1; rowIndex >= 0; rowIndex--) {
123+
const mappedInsertPos = tr.mapping.map(absoluteRowEnd) + 1;
124+
const newCells = rowNode.content.content.map((cellNode) => rebuildCell(cellNode, rowIndex));
125+
const newRow = RowType.create(rowNode.attrs, Fragment.from(newCells), rowNode.marks);
126+
tr.insert(mappedInsertPos, Fragment.from(newRow));
127+
}
128+
129+
// Now delete the original row
130+
const mappedDeleteStart = tr.mapping.map(absoluteRowStart);
131+
const mappedDeleteEnd = mappedDeleteStart + rowNode.nodeSize;
132+
tr.delete(mappedDeleteStart - 1, mappedDeleteEnd + 1);
133+
};
134+
135+
136+
const getAnnotationValue = (id, annotationValues) => {
137+
return annotationValues.find((value) => value.input_id === id)?.input_value || null;
138+
};
139+
140+
export const AnnotatorServices = {
141+
getFieldAttrs,
142+
processTables,
143+
};

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ export const FieldAnnotation = Node.create({
193193
extras: {
194194
default: {},
195195
rendered: false,
196+
},
197+
198+
generatorIndex: {
199+
default: null,
200+
rendered: false,
196201
}
197202
};
198203
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,14 @@ export const FieldAnnotation = Node.create({
272272
style: `font-size: ${fontSize}`,
273273
};
274274
},
275+
275276
},
276277
/// Formatting attrs - end.
278+
279+
generatorIndex: {
280+
rendered: false,
281+
default: null,
282+
}
277283
};
278284
},
279285

packages/super-editor/src/extensions/table/table.js

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -298,76 +298,6 @@ export const Table = Node.create({
298298

299299
return true;
300300
},
301-
302-
generateTable: (annotation, matchingAnnotation, fieldData) => {
303-
return ({ tr, editor }) => {
304-
const seenTableRowAnnotationIds = [];
305-
const { state: { doc, schema } } = editor;
306-
const { tableRow: RowType, tableCell: CellType, fieldAnnotation: FieldType, paragraph: ParaType } = schema.nodes;
307-
308-
// Locate the parent row node containing the annotation
309-
const position = doc.resolve(annotation.pos);
310-
const rowInfo = findParentNodeClosestToPos(position, (node) => node.type === RowType);
311-
if (!rowInfo) return;
312-
313-
// Get all annotations in the row
314-
rowInfo.node.descendants((node, pos) => {
315-
if (node.type.name === 'fieldAnnotation') {
316-
seenTableRowAnnotationIds.push(node.attrs.fieldId);
317-
}
318-
});
319-
320-
if (!seenTableRowAnnotationIds.includes(annotation.node.attrs.fieldId)) return [];
321-
322-
// Figure out the position where we will start inserting new rows
323-
const { pos: rowStartPos, node: rowNode } = rowInfo;
324-
let insertPos = rowStartPos + rowNode.nodeSize;
325-
326-
// Helper: rebuild a single cell for a given row index
327-
const rebuildCell = (cellNode, rowIndex) => {
328-
const updatedBlocks = cellNode.content.content.map((blockNode) => {
329-
if (blockNode.type !== ParaType) return blockNode;
330-
331-
// Rebuild paragraphs by mapping inline nodes
332-
const updatedInlines = blockNode.content.content.map((inlineNode) => {
333-
if (inlineNode.type !== FieldType) return inlineNode;
334-
335-
// Find the matching field data and compute new attributes
336-
const fieldRecord = fieldData.find((f) => f.input_id === inlineNode.attrs.fieldId);
337-
const value = fieldRecord?.input_value[rowIndex];
338-
339-
// Different field types require different annotation handling
340-
// We use the helper here to get the correct attributes
341-
// Since generated tables contain annotated fields
342-
const extraAttrs = getFieldAttrs(inlineNode, value);
343-
return FieldType.create(
344-
{ ...inlineNode.attrs, ...extraAttrs },
345-
inlineNode.content,
346-
inlineNode.marks
347-
);
348-
});
349-
350-
return ParaType.create(blockNode.attrs, Fragment.from(updatedInlines), blockNode.marks);
351-
});
352-
353-
return CellType.create(cellNode.attrs, Fragment.from(updatedBlocks), cellNode.marks);
354-
};
355-
356-
// Iterate over each row value and build+insert a new row
357-
matchingAnnotation.input_value.forEach((_, rowIndex) => {
358-
// Build all cells for the new row
359-
const newCells = rowNode.content.content.map((cellNode) => rebuildCell(cellNode, rowIndex));
360-
const newRow = RowType.create(rowNode.attrs, Fragment.from(newCells), rowNode.marks);
361-
362-
tr.insert(insertPos, Fragment.from(newRow));
363-
insertPos += newRow.nodeSize;
364-
});
365-
366-
// Remove the original (placeholder) row
367-
tr.delete(rowStartPos, rowStartPos + rowNode.nodeSize);
368-
return seenTableRowAnnotationIds;
369-
};
370-
},
371301
};
372302
},
373303

0 commit comments

Comments
 (0)