@atlaskit/editor-plugin-fragment
Version:
Fragment plugin for @atlaskit/editor-core
114 lines (109 loc) • 3.63 kB
JavaScript
/**
* This plugin ensures that certain nodes (such as tables, and various extension ones)
* have a unique `localId` attribute value for `fragment` marks.
* It also ensures the preservation of these IDs when nodes are being cut-and-pasted
* around the document.
*
* The implementation has been _heavily_ borrowed from
* - packages/editor/editor-core/src/plugins/table/pm-plugins/table-local-id.ts
*/
import { uuid } from '@atlaskit/adf-schema';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { getChangedNodes } from '@atlaskit/editor-common/utils';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
const pluginKey = new PluginKey('fragmentMarkConsistencyPlugin');
const getNodesSupportingFragmentMark = schema => {
const {
table,
extension,
bodiedExtension,
inlineExtension
} = schema.nodes;
return [table, extension, bodiedExtension, inlineExtension];
};
/**
* Ensures presence of `fragment` mark on certain node types and the uniqueness of their `localId` attributes
*/
export const createPlugin = dispatch => new SafePlugin({
key: pluginKey,
appendTransaction: (transactions, _oldState, newState) => {
let modified = false;
const tr = newState.tr;
const {
fragment
} = newState.schema.marks;
const supportedNodeTypes = getNodesSupportingFragmentMark(newState.schema);
const addedSupportedNodes = new Set();
const addedSupportedNodesPos = new Map();
const localIds = new Set();
transactions.forEach(transaction => {
if (!transaction.docChanged) {
return;
}
// Don't interfere with cut as it clashes with fixTables & we don't need
// to handle any extra cut cases in this plugin
const uiEvent = transaction.getMeta('uiEvent');
if (uiEvent === 'cut') {
return;
}
const changedNodes = getChangedNodes(transaction);
for (const {
node
} of changedNodes) {
if (!supportedNodeTypes.includes(node.type)) {
continue;
}
addedSupportedNodes.add(node);
}
});
if (!addedSupportedNodes.size) {
return;
}
// Get existing fragment marks localIds on the page
newState.doc.descendants((node, pos) => {
if (addedSupportedNodes.has(node)) {
addedSupportedNodesPos.set(node, pos);
return true;
}
if (!supportedNodeTypes.includes(node.type)) {
return true;
}
const existingFragmentMark = node.marks.find(mark => mark.type === fragment);
if (!existingFragmentMark) {
// continue traversing
return true;
}
localIds.add(existingFragmentMark.attrs.localId);
return true;
});
// If an added node has localId that collides with existing node, generate new localId
for (const node of addedSupportedNodes) {
const pos = addedSupportedNodesPos.get(node);
if (pos === undefined) {
continue;
}
const existingFragmentMark = node.marks.find(mark => mark.type === fragment);
if (!existingFragmentMark) {
continue;
}
if (localIds.has(existingFragmentMark.attrs.localId)) {
tr.setNodeMarkup(pos, undefined, node.attrs, node.marks.map(mark => {
if (mark.type !== fragment) {
return mark;
}
const fragmentMark = fragment.create({
...mark.attrs,
localId: uuid.generate(),
name: null
});
return fragmentMark;
}));
modified = true;
}
}
if (modified) {
return tr;
}
return;
}
});