|
| 1 | +import { Fragment } from 'prosemirror-model'; |
| 2 | + |
1 | 3 | /** |
2 | 4 | * Get the field attributes based on the field type and value |
3 | 5 | * |
@@ -25,3 +27,117 @@ const annotateHtml = (value) => ({ rawHtml: value }); |
25 | 27 | const annotateText = (value) => ({ displayLabel: value }); |
26 | 28 | const annotateImage = (value) => ({ imageSrc: value }); |
27 | 29 | 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 | +}; |
0 commit comments