Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions packages/super-editor/src/core/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

}
146 changes: 140 additions & 6 deletions packages/super-editor/src/core/helpers/annotator.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -7,39 +8,59 @@ 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 });
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
*
* @param {Object} param0 The editor instance and annotation values
* @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 = [];
Expand All @@ -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;
Expand Down Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down
Loading