@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
141 lines (134 loc) • 4.19 kB
JavaScript
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { findParentNodeOfTypeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
/**
* Tracks changes to sync blocks in a transaction.
* @param predicate - A function that returns true if a node is a sync block (source or reference or both).
* @param tr - The transaction to track changes in.
* @param state - The editor state.
* @returns An object containing the removed and added sync blocks.
*/
export const trackSyncBlocks = (predicate, tr, state) => {
const removed = {};
const added = {};
if (!tr.docChanged) {
return {
removed: [],
added: []
};
}
// and cast to specific step types
const replaceSteps = tr.steps.filter(step => step instanceof ReplaceStep || step instanceof ReplaceAroundStep);
// this is a quick check to see if any insertion/deletion of sync block happened
const hasSyncBlockChanges = replaceSteps.some(step => {
const {
from,
to
} = step;
const docAtStep = tr.docs[tr.steps.indexOf(step)];
let hasChange = false;
if (from !== to) {
step.getMap().forEach((oldStart, oldEnd) => {
if (oldStart !== oldEnd && !hasChange) {
const deletedSlice = docAtStep.slice(Math.max(0, oldStart), Math.min(docAtStep.content.size, oldEnd));
deletedSlice.content.forEach(node => {
if (hasChange) {
return;
}
// for top level nodes
if (predicate(node)) {
hasChange = true;
}
});
}
});
}
// no need to check insertions if we already found deletions
if (step.slice.content.size > 0 && !hasChange) {
step.slice.content.forEach(node => {
if (predicate(node) && !hasChange) {
hasChange = true;
}
});
}
return hasChange;
});
if (hasSyncBlockChanges) {
const oldDoc = state.doc;
const newDoc = tr.doc;
const syncBlockMapOld = {};
const syncBlockMapNew = {};
oldDoc.content.forEach(node => {
if (predicate(node)) {
const syncBlockAttr = node.attrs;
syncBlockMapOld[syncBlockAttr.localId] = {
attrs: syncBlockAttr
};
}
});
newDoc.content.forEach((node, offset) => {
if (predicate(node)) {
const syncBlockAttr = node.attrs;
syncBlockMapNew[syncBlockAttr.localId] = {
attrs: syncBlockAttr,
node: node,
from: offset,
to: offset + node.nodeSize
};
}
});
// Find removed sync blocks
for (const localId in syncBlockMapOld) {
if (!syncBlockMapNew[localId]) {
removed[localId] = syncBlockMapOld[localId];
}
}
// Find added sync blocks
for (const localId in syncBlockMapNew) {
if (!syncBlockMapOld[localId]) {
added[localId] = syncBlockMapNew[localId];
}
}
}
return {
removed: Object.values(removed),
added: Object.values(added)
};
};
/**
*
* @returns true if steps modifies children node within bodiedSyncBlock
*/
export const hasEditInSyncBlock = (tr, state) => {
const {
bodiedSyncBlock
} = state.schema.nodes;
for (let i = 0; i < tr.steps.length; i++) {
var _tr$docs;
const step = tr.steps[i];
const map = step.getMap();
const docAfterStep = (_tr$docs = tr.docs[i + 1]) !== null && _tr$docs !== void 0 ? _tr$docs : tr.doc;
const positions = [];
// Extract positions from steps dynamically based on applicable properties
if ('from' in step && typeof step.from === 'number' && 'to' in step && typeof step.to === 'number') {
const {
from,
to
} = step;
positions.push(from, to);
} else if ('pos' in step && typeof step.pos === 'number') {
const {
pos
} = step;
positions.push(pos);
}
for (const pos of positions) {
const newPos = map.map(pos);
if (newPos >= 0 && newPos <= docAfterStep.content.size) {
if (findParentNodeOfTypeClosestToPos(docAfterStep.resolve(newPos), bodiedSyncBlock)) {
return true;
}
}
}
}
return false;
};