@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
190 lines (184 loc) • 5.73 kB
JavaScript
/**
* A plugin for ensuring tables always have unique local IDs, and to
* preserve/not regenerate them when they are being cut and paste around the
* document.
*
* More broadly, this plugin should be generalised so it can solve this ‘unique
* id’ problem across the codebase for any node, but for now this will live on
* its own solving only for tables.
*
* TODO: https://product-fabric.atlassian.net/browse/ED-12714
*
*/
import rafSchedule from 'raf-schd';
import { uuid } from '@atlaskit/adf-schema';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { stepHasSlice } from '@atlaskit/editor-common/utils';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
const pluginKey = new PluginKey('tableLocalIdPlugin');
const getPluginState = state => state && pluginKey.getState(state);
/**
* Ensures uniqueness of `localId`s on tables being created or edited
*/
const createPlugin = dispatch => new SafePlugin({
key: pluginKey,
state: {
init() {
return {
parsedForLocalIds: false
};
},
apply(tr, pluginState) {
const meta = tr.getMeta(pluginKey);
if (meta) {
const keys = Object.keys(meta);
const changed = keys.some(key => {
return pluginState[key] !== meta[key];
});
if (changed) {
const newState = {
...pluginState,
...meta
};
dispatch(pluginKey, newState);
return newState;
}
}
return pluginState;
}
},
view: () => {
return {
/**
* Utilise the update cycle for _one_ scan through an initial doc
* to ensure existing tables without IDs get them when this plugin is
* enabled.
*
* This entire block can be skipped if we simply remove the `checkIsAddingTable`
* check in appendTransaction, but that comes with 2 cons:
*
* 1. require a transaction before we actually add the local IDs
* 2. ever slightly more unncessary checks
*
* Finally, this never happens in practice when utilising this in
* confluence, as the collab/synchrony initialisation process will
* trigger a transaction which adds tables, and thus this plugin will
* add/dedupe the necessary IDs. But general usage of the editor
* without collab should still solve for IDs.
*/
update(editorView) {
const {
state
} = editorView;
const pluginState = getPluginState(state);
if (!pluginState) {
return;
}
const parsed = pluginState.parsedForLocalIds;
if (parsed) {
return;
}
const {
table
} = state.schema.nodes;
rafSchedule(() => {
const tr = editorView.state.tr;
let tableIdWasAdded = false;
editorView.state.doc.descendants((node, pos) => {
const isTable = node.type === table;
const localId = node.attrs.localId;
if (isTable && !localId) {
tableIdWasAdded = true;
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
localId: uuid.generate()
});
return false;
}
/**
* Otherwise continue traversing, we can encounter tables nested in
* expands/bodiedExtensions
*/
return true;
});
if (tableIdWasAdded) {
tr.setMeta('addToHistory', false);
editorView.dispatch(tr);
}
})();
editorView.dispatch(state.tr.setMeta(pluginKey, {
parsedForLocalIds: true
}));
}
};
},
appendTransaction: (transactions, _oldState, newState) => {
let modified = false;
const tr = newState.tr;
const {
table
} = newState.schema.nodes;
const addedTableNodes = new Set();
const addedTableNodePos = 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;
}
/** Get the tables we are adding and their position */
for (const step of transaction.steps) {
if (!stepHasSlice(step)) {
continue;
}
step.slice.content.descendants(node => {
if (node.type === table) {
addedTableNodes.add(node);
}
return true;
});
}
});
if (!addedTableNodes.size) {
return;
}
// Get the existing localIds on the page
newState.doc.descendants((node, pos) => {
// Skip if this is position of added table
if (addedTableNodes.has(node)) {
addedTableNodePos.set(node, pos);
return false;
}
if (node.type !== table) {
return true;
}
localIds.add(node.attrs.localId);
// can't have table within a table
return false;
});
// If the added nodes have duplicate id, generate a new one
for (const node of addedTableNodes) {
if (!node.attrs.localId || localIds.has(node.attrs.localId)) {
const pos = addedTableNodePos.get(node);
if (pos === undefined) {
continue;
}
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
localId: uuid.generate()
});
modified = true;
}
}
if (modified) {
return tr;
}
return;
}
});
export { createPlugin };