From a7be6d338e8cbb8504dc79a1d3f98dbf7ddd3462 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 20 May 2025 21:40:28 -0700 Subject: [PATCH] Add annotate to Editor --- packages/super-editor/src/core/Editor.js | 31 +++- .../src/core/helpers/annotator.js | 146 +++++++++++++++++- .../trackFieldAnnotationsDeletion.js | 5 +- 3 files changed, 173 insertions(+), 9 deletions(-) diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index 9deff01477..1a1dfc19a5 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -1216,8 +1216,35 @@ export class Editor extends EventEmitter { * @param {Array[Object]} annotationValues * @returns {void} */ - prepareForAnnotations(annotationValues = []) { - AnnotatorServices.processTables({ editor: this, annotationValues }); + prepareForAnnotations(annotationValues = [], hiddenIds = []) { + const { tr } = this.state; + const { dispatch } = this.view; + const newTr = AnnotatorServices.processTables({ editor: this, tr, annotationValues, hiddenIds }); + this.view.dispatch(newTr); + } + + /** + * Annotate the document with the given annotation values. + * + * @param {Array[Object]} annotationValues + * @param {Array[String]} hiddenIds + */ + annotate(annotationValues = [], hiddenIds = []) { + const { state, view, schema } = this; + let tr = state.tr; + + tr = AnnotatorServices.processTables({ editor: this, tr, annotationValues }); + tr = AnnotatorServices.annotateDocument({ + tr, + schema, + annotationValues, + hiddenFieldIds: hiddenIds + }); + + // 3) Finally dispatch *once*: + if (tr.docChanged) { + view.dispatch(tr.scrollIntoView()); + } } } diff --git a/packages/super-editor/src/core/helpers/annotator.js b/packages/super-editor/src/core/helpers/annotator.js index 9f0df76e61..25e05623b3 100644 --- a/packages/super-editor/src/core/helpers/annotator.js +++ b/packages/super-editor/src/core/helpers/annotator.js @@ -1,4 +1,5 @@ import { Fragment } from 'prosemirror-model'; +import { fieldAnnotationHelpers } from '@extensions/index.js'; /** * Get the field attributes based on the field type and value @@ -7,20 +8,23 @@ import { Fragment } from 'prosemirror-model'; * @param {Object} value The value we want to annotate the field with * @returns */ -export const getFieldAttrs = (field, value) => { +export const getFieldAttrs = (field, value, input) => { const { type } = field.attrs; const annotatorHandlers = { html: annotateHtml, text: annotateText, checkbox: annotateCheckbox, image: annotateImage, + link: annotateLink, + yesno: annotateYesNo, + date: annotateDate, } const handler = annotatorHandlers[type]; if (!handler) return {}; // Run the handler to get the annotated field attributes - return handler(value); + return handler(value, input); }; const annotateHtml = (value) => ({ rawHtml: value }); @@ -28,6 +32,25 @@ const annotateText = (value) => ({ displayLabel: value }); const annotateImage = (value) => ({ imageSrc: value }); const annotateCheckbox = (value) => ({ displayLabel: value }); +const annotateDate = (value, input) => { + const formatted = getFormattedDate(value, input.input_format); + return { displayLabel: formatted }; +}; + +const annotateLink = (value) => { + if (!value.startsWith('http')) value = `http://${value}`; + return { linkUrl: value }; +}; + +const annotateYesNo = (value) => { + const yesNoValues = { + 'YES': 'Yes', + 'NO': 'No', + } + const parsedValue = yesNoValues[value[0].toUpperCase()]; + return { displayLabel: parsedValue }; +}; + /** * Pre-process tables in the document to generate rows from annotations if necessary * @@ -35,11 +58,9 @@ const annotateCheckbox = (value) => ({ displayLabel: value }); * @param {Object} param0.editor The editor instance * @param {Array} param0.annotationValues The annotation values to process */ -export const processTables = ({ editor, annotationValues }) => { +export const processTables = ({ editor, tr, annotationValues }) => { const { state } = editor; const { doc } = state; - const { tr } = state; - const { dispatch } = editor.view; // Get all tables in the document const tables = []; @@ -50,8 +71,10 @@ export const processTables = ({ editor, annotationValues }) => { tables.reverse().forEach((table) => { generateTableIfNecessary({ tableNode: table, annotationValues, tr, editor }); }); - dispatch(tr); + + return tr; }; + const generateTableIfNecessary = ({ tableNode, annotationValues, tr, editor }) => { let rowNodeToGenerate = null; let currentRow = null; @@ -137,7 +160,118 @@ const getAnnotationValue = (id, annotationValues) => { return annotationValues.find((value) => value.input_id === id)?.input_value || null; }; +export const annotateDocument = ({ + annotationValues = [], + hiddenFieldIds = [], + schema, + tr, +}) => { + + const annotations = []; + const FieldType = schema.nodes.fieldAnnotation; + tr.doc.descendants((node, pos) => { + if (node.type === FieldType) { + annotations.push({ node, pos, size: node.nodeSize }); + } + }); + + const toDelete = new Set(); + + if (hiddenFieldIds.length) { + for (const { node, pos } of annotations) { + if (hiddenFieldIds.includes(node.attrs.fieldId)) { + toDelete.add(pos); + } + } + } + + // For each annotation, either queue it for deletion or queue an update + for (const { node, pos } of annotations) { + const { type, fieldType, fieldId } = node.attrs; + if (toDelete.has(pos)) continue; + + let newValue = null; + const input = annotationValues.find(i => i.input_id === fieldId); + + if (!input) { + const checkboxInputs = annotationValues.filter( + i => i.input_field_type === 'CHECKBOXINPUT' + ); + inputsLoop: + for (const cb of checkboxInputs) { + for (const opt of cb.input_options) { + if (opt.itemid === fieldId) { + newValue = cb.input_link_value[opt.itemid] || ' '; + break inputsLoop; + } + } + } + } + newValue = newValue || input?.input_value || null; + + // skip table-generator placeholders + if (Array.isArray(newValue) && node.attrs.generatorIndex != null) { + continue; + } + + if (type === 'checkbox' || fieldType === 'CHECKBOXINPUT') { + const isEmptyOrSquare = !newValue + || (typeof newValue === 'string' && newValue.codePointAt(0) === 0x2610); + if (isEmptyOrSquare) newValue = ' '; + } + + // queue delete or update + if (!newValue) { + toDelete.add(pos); + } else { + const attrs = getFieldAttrs(node, newValue, input); + tr = tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + ...attrs + }); + } + } + + // perform deletes all in one go (descending positions) + Array.from(toDelete) + .sort((a, b) => b - a) + .forEach(pos => { + const ann = annotations.find(a => a.pos === pos); + if (!ann) return; + tr = tr.delete(pos, pos + ann.node.nodeSize); + }); + + return tr; +}; + +/** + * Format the date to the given format + * + * @param {String} input The date value + * @param {String} format The date format + */ +const getFormattedDate = (input = null, format = '') => { + // 1. Parse: if input is falsy, use "now"; otherwise let Date handle it. + const date = input ? new Date(input) : new Date(); + + // 2. If invalid, just return what you got. + if (isNaN(date.getTime())) { + return input; + } + + // 3. If a custom format was requested, use the dateFormat lib: + if (format) return dateFormat(date, format); + + // 4. Otherwise, do a single toLocaleDateString call: + return date.toLocaleDateString('en-US', { + month: 'short', // e.g. “May” + day: '2-digit', // e.g. “05” + year: 'numeric' // e.g. “2025” + }); +}; + export const AnnotatorServices = { getFieldAttrs, processTables, + annotateDocument, }; diff --git a/packages/super-editor/src/extensions/field-annotation/fieldAnnotationHelpers/trackFieldAnnotationsDeletion.js b/packages/super-editor/src/extensions/field-annotation/fieldAnnotationHelpers/trackFieldAnnotationsDeletion.js index e44d79aca9..dce9e15f51 100644 --- a/packages/super-editor/src/extensions/field-annotation/fieldAnnotationHelpers/trackFieldAnnotationsDeletion.js +++ b/packages/super-editor/src/extensions/field-annotation/fieldAnnotationHelpers/trackFieldAnnotationsDeletion.js @@ -1,7 +1,10 @@ import { findRemovedFieldAnnotations } from './findRemovedFieldAnnotations.js'; export function trackFieldAnnotationsDeletion(editor, tr) { - let removedAnnotations = findRemovedFieldAnnotations(tr); + let removedAnnotations = []; + try { + removedAnnotations = findRemovedFieldAnnotations(tr); + } catch (e) {} if (removedAnnotations.length > 0) { setTimeout(() => {