in src/plugin/fieldViews/ProseMirrorFieldView.ts [140:266]
private updateInnerEditor(
node: Node,
decorations: DecorationSource,
elementOffset: number,
selection?: Selection,
storedMarks?: readonly Mark[] | null
) {
if (!this.innerEditorView) {
return;
}
this.offset = elementOffset;
this.node = node;
this.applyDecorationsFromOuterEditor(decorations, node);
const state = this.innerEditorView.state;
let shouldDispatchTransaction = false;
let tr = state.tr;
// Check if the inner selection needs to be updated
if (selection) {
// Absolute positions of the incoming selection
const incomingAnchorPos = selection.$anchor.pos;
const incomingHeadPos = selection.$head.pos;
// Relative positions of the current selection in the inner editor
const currentAnchorPos = this.innerEditorView.state.selection.$anchor.pos;
const currentHeadPos = this.innerEditorView.state.selection.$head.pos;
// Absolute position of the field in the document
// Note: we must offset to account for a few things:
// - getPos() returns the position directly before the parent node (+1)
// - the node we will be altering is a child of its parent (+1)
const contentOffset = 2;
const fieldStart = this.offset + this.getPos() + contentOffset;
const fieldEnd =
this.offset + this.getPos() + this.innerEditorView.state.doc.nodeSize;
const incomingSelectionIsWithinThisField =
incomingAnchorPos > fieldStart &&
incomingAnchorPos < fieldEnd &&
incomingHeadPos > fieldStart &&
incomingHeadPos < fieldEnd;
// The inner editor's selection will be offset relative to the start of this field,
// compared to the incoming selection
const incomingSelectionDiffersFromCurrentSelection =
currentAnchorPos !== incomingAnchorPos - fieldStart ||
currentHeadPos !== incomingHeadPos - fieldStart;
if (
incomingSelectionIsWithinThisField &&
incomingSelectionDiffersFromCurrentSelection
) {
const offsetMap = StepMap.offset(-fieldStart);
const mappedSelection = selection.map(state.tr.doc, offsetMap);
shouldDispatchTransaction = true;
tr = tr.setSelection(mappedSelection);
}
}
// Check if the passed-in Node is different to the existing content,
// figure out the smallest change to the node content we need to make to
// successfully update the inner editor, and apply it.
const diffStart = node.content.findDiffStart(state.doc.content);
const diffEnd = node.content.findDiffEnd(state.doc.content);
// Check for null specifically, rather than falsiness, because a diff starting at pos 0 is a valid diff
if (diffStart !== null && diffEnd !== null) {
let { a: endOfOuterDiff, b: endOfInnerDiff } = diffEnd;
// This overlap accounts for a situation where we're diffing nodes where we encounter
// identical content.
//
// For example, if the inner node has content 'a', and the outer node has content 'aa',
// `diffStart` sees (numbers are positions, ^ denotes the value found by the method)
//
// ab inner node
// abb outer node
// 123
// ^
//
// `diffEnd` for the inner node is
// ab inner node
// abb outer node
// 123
// ^
//
// But 2 is not the correct end of the diff. The correct diff is (3, 3).
//
// This happens because `findDiffEnd` finds the first point at which the content differs,
// starting from the end of the nodes. So if we encounter identical content, the diff will
// be shorter by the length of the identical content we encounter – or the overlap between
// the two nodes from the point of view of the diff.
const overlap = diffStart - Math.min(endOfOuterDiff, endOfInnerDiff);
if (overlap > 0) {
endOfOuterDiff += overlap;
endOfInnerDiff += overlap;
}
shouldDispatchTransaction = true;
tr = tr.replace(
diffStart,
endOfInnerDiff,
node.slice(diffStart, endOfOuterDiff)
);
}
const storedMarksHaveChanged = !isEqual(
storedMarks,
this.innerEditorView.state.storedMarks
);
if (storedMarksHaveChanged && storedMarks !== undefined) {
shouldDispatchTransaction = true;
tr = tr.setStoredMarks(storedMarks);
}
if (shouldDispatchTransaction) {
tr = tr.setMeta("fromOutside", true);
this.innerEditorView.dispatch(tr);
} else {
return this.maybeRerenderDecorations();
}
}