Skip to content

Commit 9a467c6

Browse files
authored
Merge pull request #517 from Harbour-Enterprises/nick/har-9715-annotate
HAR-9715 - Add annotate to Editor
2 parents 32454e0 + a7be6d3 commit 9a467c6

3 files changed

Lines changed: 173 additions & 9 deletions

File tree

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,8 +1216,35 @@ export class Editor extends EventEmitter {
12161216
* @param {Array[Object]} annotationValues
12171217
* @returns {void}
12181218
*/
1219-
prepareForAnnotations(annotationValues = []) {
1220-
AnnotatorServices.processTables({ editor: this, annotationValues });
1219+
prepareForAnnotations(annotationValues = [], hiddenIds = []) {
1220+
const { tr } = this.state;
1221+
const { dispatch } = this.view;
1222+
const newTr = AnnotatorServices.processTables({ editor: this, tr, annotationValues, hiddenIds });
1223+
this.view.dispatch(newTr);
1224+
}
1225+
1226+
/**
1227+
* Annotate the document with the given annotation values.
1228+
*
1229+
* @param {Array[Object]} annotationValues
1230+
* @param {Array[String]} hiddenIds
1231+
*/
1232+
annotate(annotationValues = [], hiddenIds = []) {
1233+
const { state, view, schema } = this;
1234+
let tr = state.tr;
1235+
1236+
tr = AnnotatorServices.processTables({ editor: this, tr, annotationValues });
1237+
tr = AnnotatorServices.annotateDocument({
1238+
tr,
1239+
schema,
1240+
annotationValues,
1241+
hiddenFieldIds: hiddenIds
1242+
});
1243+
1244+
// 3) Finally dispatch *once*:
1245+
if (tr.docChanged) {
1246+
view.dispatch(tr.scrollIntoView());
1247+
}
12211248
}
12221249

12231250
}

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

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Fragment } from 'prosemirror-model';
2+
import { fieldAnnotationHelpers } from '@extensions/index.js';
23

34
/**
45
* Get the field attributes based on the field type and value
@@ -7,39 +8,59 @@ import { Fragment } from 'prosemirror-model';
78
* @param {Object} value The value we want to annotate the field with
89
* @returns
910
*/
10-
export const getFieldAttrs = (field, value) => {
11+
export const getFieldAttrs = (field, value, input) => {
1112
const { type } = field.attrs;
1213
const annotatorHandlers = {
1314
html: annotateHtml,
1415
text: annotateText,
1516
checkbox: annotateCheckbox,
1617
image: annotateImage,
18+
link: annotateLink,
19+
yesno: annotateYesNo,
20+
date: annotateDate,
1721
}
1822

1923
const handler = annotatorHandlers[type];
2024
if (!handler) return {};
2125

2226
// Run the handler to get the annotated field attributes
23-
return handler(value);
27+
return handler(value, input);
2428
};
2529

2630
const annotateHtml = (value) => ({ rawHtml: value });
2731
const annotateText = (value) => ({ displayLabel: value });
2832
const annotateImage = (value) => ({ imageSrc: value });
2933
const annotateCheckbox = (value) => ({ displayLabel: value });
3034

35+
const annotateDate = (value, input) => {
36+
const formatted = getFormattedDate(value, input.input_format);
37+
return { displayLabel: formatted };
38+
};
39+
40+
const annotateLink = (value) => {
41+
if (!value.startsWith('http')) value = `http://${value}`;
42+
return { linkUrl: value };
43+
};
44+
45+
const annotateYesNo = (value) => {
46+
const yesNoValues = {
47+
'YES': 'Yes',
48+
'NO': 'No',
49+
}
50+
const parsedValue = yesNoValues[value[0].toUpperCase()];
51+
return { displayLabel: parsedValue };
52+
};
53+
3154
/**
3255
* Pre-process tables in the document to generate rows from annotations if necessary
3356
*
3457
* @param {Object} param0 The editor instance and annotation values
3558
* @param {Object} param0.editor The editor instance
3659
* @param {Array} param0.annotationValues The annotation values to process
3760
*/
38-
export const processTables = ({ editor, annotationValues }) => {
61+
export const processTables = ({ editor, tr, annotationValues }) => {
3962
const { state } = editor;
4063
const { doc } = state;
41-
const { tr } = state;
42-
const { dispatch } = editor.view;
4364

4465
// Get all tables in the document
4566
const tables = [];
@@ -50,8 +71,10 @@ export const processTables = ({ editor, annotationValues }) => {
5071
tables.reverse().forEach((table) => {
5172
generateTableIfNecessary({ tableNode: table, annotationValues, tr, editor });
5273
});
53-
dispatch(tr);
74+
75+
return tr;
5476
};
77+
5578
const generateTableIfNecessary = ({ tableNode, annotationValues, tr, editor }) => {
5679
let rowNodeToGenerate = null;
5780
let currentRow = null;
@@ -137,7 +160,118 @@ const getAnnotationValue = (id, annotationValues) => {
137160
return annotationValues.find((value) => value.input_id === id)?.input_value || null;
138161
};
139162

163+
export const annotateDocument = ({
164+
annotationValues = [],
165+
hiddenFieldIds = [],
166+
schema,
167+
tr,
168+
}) => {
169+
170+
const annotations = [];
171+
const FieldType = schema.nodes.fieldAnnotation;
172+
tr.doc.descendants((node, pos) => {
173+
if (node.type === FieldType) {
174+
annotations.push({ node, pos, size: node.nodeSize });
175+
}
176+
});
177+
178+
const toDelete = new Set();
179+
180+
if (hiddenFieldIds.length) {
181+
for (const { node, pos } of annotations) {
182+
if (hiddenFieldIds.includes(node.attrs.fieldId)) {
183+
toDelete.add(pos);
184+
}
185+
}
186+
}
187+
188+
// For each annotation, either queue it for deletion or queue an update
189+
for (const { node, pos } of annotations) {
190+
const { type, fieldType, fieldId } = node.attrs;
191+
if (toDelete.has(pos)) continue;
192+
193+
let newValue = null;
194+
const input = annotationValues.find(i => i.input_id === fieldId);
195+
196+
if (!input) {
197+
const checkboxInputs = annotationValues.filter(
198+
i => i.input_field_type === 'CHECKBOXINPUT'
199+
);
200+
inputsLoop:
201+
for (const cb of checkboxInputs) {
202+
for (const opt of cb.input_options) {
203+
if (opt.itemid === fieldId) {
204+
newValue = cb.input_link_value[opt.itemid] || ' ';
205+
break inputsLoop;
206+
}
207+
}
208+
}
209+
}
210+
newValue = newValue || input?.input_value || null;
211+
212+
// skip table-generator placeholders
213+
if (Array.isArray(newValue) && node.attrs.generatorIndex != null) {
214+
continue;
215+
}
216+
217+
if (type === 'checkbox' || fieldType === 'CHECKBOXINPUT') {
218+
const isEmptyOrSquare = !newValue
219+
|| (typeof newValue === 'string' && newValue.codePointAt(0) === 0x2610);
220+
if (isEmptyOrSquare) newValue = ' ';
221+
}
222+
223+
// queue delete or update
224+
if (!newValue) {
225+
toDelete.add(pos);
226+
} else {
227+
const attrs = getFieldAttrs(node, newValue, input);
228+
tr = tr.setNodeMarkup(pos, undefined, {
229+
...node.attrs,
230+
...attrs
231+
});
232+
}
233+
}
234+
235+
// perform deletes all in one go (descending positions)
236+
Array.from(toDelete)
237+
.sort((a, b) => b - a)
238+
.forEach(pos => {
239+
const ann = annotations.find(a => a.pos === pos);
240+
if (!ann) return;
241+
tr = tr.delete(pos, pos + ann.node.nodeSize);
242+
});
243+
244+
return tr;
245+
};
246+
247+
/**
248+
* Format the date to the given format
249+
*
250+
* @param {String} input The date value
251+
* @param {String} format The date format
252+
*/
253+
const getFormattedDate = (input = null, format = '') => {
254+
// 1. Parse: if input is falsy, use "now"; otherwise let Date handle it.
255+
const date = input ? new Date(input) : new Date();
256+
257+
// 2. If invalid, just return what you got.
258+
if (isNaN(date.getTime())) {
259+
return input;
260+
}
261+
262+
// 3. If a custom format was requested, use the dateFormat lib:
263+
if (format) return dateFormat(date, format);
264+
265+
// 4. Otherwise, do a single toLocaleDateString call:
266+
return date.toLocaleDateString('en-US', {
267+
month: 'short', // e.g. “May”
268+
day: '2-digit', // e.g. “05”
269+
year: 'numeric' // e.g. “2025”
270+
});
271+
};
272+
140273
export const AnnotatorServices = {
141274
getFieldAttrs,
142275
processTables,
276+
annotateDocument,
143277
};

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { findRemovedFieldAnnotations } from './findRemovedFieldAnnotations.js';
22

33
export function trackFieldAnnotationsDeletion(editor, tr) {
4-
let removedAnnotations = findRemovedFieldAnnotations(tr);
4+
let removedAnnotations = [];
5+
try {
6+
removedAnnotations = findRemovedFieldAnnotations(tr);
7+
} catch (e) {}
58

69
if (removedAnnotations.length > 0) {
710
setTimeout(() => {

0 commit comments

Comments
 (0)