@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
259 lines (250 loc) • 11.8 kB
JavaScript
/**
* This plugin allows sorting of table nodes in the Editor without modifying the underlying ProseMirror document.
* Instead of making changes to the ProseMirror document, the plugin sorts the table rows in the DOM. This allows the sorting to be
* visible to the user without affecting the document's content.
*/
import { createElement } from 'react';
import { RawIntlProvider } from 'react-intl';
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid/v4';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { SortOrder } from '@atlaskit/editor-common/types';
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { SortingIconWrapper } from '../../ui/icons/SortingIconWrapper';
import { getPluginState } from '../plugin-factory';
import { IS_DISABLED_CLASS_NAME, SORT_INDEX_DATA_ATTRIBUTE, SORTING_ICON_CLASS_NAME } from './consts';
import { tableViewModeSortPluginKey as key } from './plugin-key';
import { getTableElements, toggleSort } from './utils';
export const createPlugin = (api, nodeViewPortalProviderAPI) => {
return new SafePlugin({
state: {
init: () => ({
decorations: DecorationSet.empty,
sort: {},
allTables: []
}),
apply(tr, pluginState, oldState) {
var _api$editorViewMode;
// TODO: ED-26961 - move this mode check to plugin creation if possible. Right now it's here because the initial state
// does not appear correct when the plugin is created.
const {
mode
} = ((_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.currentState()) || {};
if (mode !== 'view') {
var _pluginState$decorati, _pluginState$decorati2;
const sortingDecorations = pluginState === null || pluginState === void 0 ? void 0 : (_pluginState$decorati = pluginState.decorations) === null || _pluginState$decorati === void 0 ? void 0 : _pluginState$decorati.find(undefined, undefined, s => (s === null || s === void 0 ? void 0 : s.type) === 'sorting-decoration');
return {
...pluginState,
decorations: pluginState === null || pluginState === void 0 ? void 0 : (_pluginState$decorati2 = pluginState.decorations) === null || _pluginState$decorati2 === void 0 ? void 0 : _pluginState$decorati2.remove(sortingDecorations)
};
}
let {
decorations,
sort,
allTables
} = pluginState;
const sortMeta = tr.getMeta('tableSortMeta');
const hoverTableMeta = tr.getMeta('mouseEnterTable');
const removeTableMeta = tr.getMeta('removeTable');
let tableId = '';
// Remove the table from the state
if (removeTableMeta) {
allTables = allTables.filter(([id]) => id !== removeTableMeta);
} else {
tableId = hoverTableMeta === null || hoverTableMeta === void 0 ? void 0 : hoverTableMeta[0];
}
sort = {
...sort,
...sortMeta
};
const isTableInState = allTables.some(([id]) => id === tableId);
// Update the table in the state
if (hoverTableMeta) {
allTables = allTables.filter(([id]) => id !== hoverTableMeta[0]);
allTables.push(hoverTableMeta);
}
/**
* Create decorations for the sorting icons
*/
const decs = [];
const sortingDecorations = pluginState.decorations.find(undefined, undefined, spec => spec.tableId === tableId && spec.type === 'sorting-decoration');
// TODO: ED-26961 - add support for keyboard only users
if (hoverTableMeta && !isTableInState || sortMeta || isTableInState && !sortingDecorations.length) {
allTables.forEach(table => {
const [tableId, _node, pos] = table;
const tableNode = tr.doc.nodeAt(tr.mapping.map(pos));
if (!tableNode || tableNode.type.name !== 'table') {
return pluginState;
}
const map = TableMap.get(tableNode);
const hasMergedCells = new Set(map.map).size !== map.map.length;
map.mapByRow[0].forEach((cell, index) => {
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
const decorationRenderKey = uuid();
decs.push(Decoration.widget(cell + pos + 2, () => {
var _sort$tableId;
const element = document.createElement('div');
element.setAttribute(SORT_INDEX_DATA_ATTRIBUTE, `${index}`);
element.classList.add(SORTING_ICON_CLASS_NAME);
if (hasMergedCells) {
element.classList.add(IS_DISABLED_CLASS_NAME);
}
let sortOrdered;
if (index === ((_sort$tableId = sort[tableId]) === null || _sort$tableId === void 0 ? void 0 : _sort$tableId.index)) {
var _sort$tableId2;
sortOrdered = (_sort$tableId2 = sort[tableId]) === null || _sort$tableId2 === void 0 ? void 0 : _sort$tableId2.direction;
} else {
sortOrdered = SortOrder.NO_ORDER;
}
const {
getIntl
} = getPluginState(oldState);
nodeViewPortalProviderAPI.render(() => /*#__PURE__*/createElement(RawIntlProvider, {
value: getIntl()
}, /*#__PURE__*/createElement(SortingIconWrapper, {
isSortingAllowed: !hasMergedCells,
sortOrdered,
onClick: () => {},
onKeyDown: () => {},
api
})), element, decorationRenderKey);
return element;
}, {
destroy: node => {
nodeViewPortalProviderAPI.remove(decorationRenderKey);
},
type: 'sorting-decoration',
tableId
}));
});
});
decorations = DecorationSet.create(tr.doc, decs);
}
/**
* Map the decorations to the new document if there are changes
*/
if (tr.docChanged) {
decorations = decorations.map(tr.mapping, tr.doc);
allTables = allTables.map(table => {
return [table[0], table[1], tr.mapping.map(table[2])];
});
}
return {
decorations,
sort,
allTables
};
}
},
key,
appendTransaction: (trs, oldState, newState) => {
var _api$editorViewMode2, _key$getState;
// return newState.tr;
const {
mode
} = (api === null || api === void 0 ? void 0 : (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.sharedState.currentState()) || {};
if (mode !== 'view') {
return newState.tr;
}
let allTables = ((_key$getState = key.getState(newState)) === null || _key$getState === void 0 ? void 0 : _key$getState.allTables) || [];
/**
* If incoming changes have affected a table node, remove the sorting. This prevents the
* table from breaking if changes like merged cells are incoming.
*/
for (const tr of trs) {
const hoverTableMeta = tr.getMeta('mouseEnterTable');
if (hoverTableMeta) {
allTables = allTables.filter(([id]) => id !== hoverTableMeta[0]);
allTables.push(hoverTableMeta);
}
const isRemote = tr.getMeta('isRemote');
const isDocChanged = tr.docChanged;
const isChangesIncoming = isRemote && isDocChanged;
const oldPluginState = key.getState(oldState);
const newPluginState = key.getState(newState);
for (const table of allTables) {
var _oldPluginState$sort, _newPluginState$sort;
const [tableId, node, pos] = table;
const {
order: oldOrder,
direction: oldDirection,
index: oldIndex
} = (oldPluginState === null || oldPluginState === void 0 ? void 0 : (_oldPluginState$sort = oldPluginState.sort) === null || _oldPluginState$sort === void 0 ? void 0 : _oldPluginState$sort[tableId]) || {};
if (isChangesIncoming) {
var _maybeTableNode$attrs;
const maybeTableNode = tr.doc.nodeAt(pos);
const isTableNodeChanged = (maybeTableNode === null || maybeTableNode === void 0 ? void 0 : (_maybeTableNode$attrs = maybeTableNode.attrs) === null || _maybeTableNode$attrs === void 0 ? void 0 : _maybeTableNode$attrs.localId) !== tableId || !node.eq(maybeTableNode);
if (isTableNodeChanged) {
const newtr = newState.tr;
newtr.setMeta('tableSortMeta', {
[tableId]: {}
});
newtr.setMeta('removeTable', tableId);
// Unsort the table here
if (oldOrder !== undefined) {
const {
rows,
tbody
} = getTableElements(tableId);
if (!rows || !tbody) {
return newtr;
}
const sortedOrder = [...oldOrder].sort((a, b) => a.value - b.value);
sortedOrder.forEach((index, i) => {
tbody.appendChild(rows[index.index + 1]);
});
return newtr;
}
}
}
/**
* Sort the table if the sort order has changed
*/
const {
order: newOrder,
direction: newDirection,
index: newIndex
} = (newPluginState === null || newPluginState === void 0 ? void 0 : (_newPluginState$sort = newPluginState.sort) === null || _newPluginState$sort === void 0 ? void 0 : _newPluginState$sort[tableId]) || {};
const orderChanged = oldDirection !== newDirection || oldIndex !== newIndex;
if (orderChanged) {
if (!isRemote && newDirection !== SortOrder.NO_ORDER) {
const {
rows,
tbody
} = getTableElements(tableId);
if (rows && newOrder) {
newOrder.forEach((index, i) => {
tbody === null || tbody === void 0 ? void 0 : tbody.appendChild(rows[index.value + 1]);
});
}
}
}
}
}
return newState.tr;
},
props: {
handleDOMEvents: {
keydown: (view, event) => {
// TODO: ED-26961 - fix the focus issue here, where toggling sort with a keypress loses focus
if (event.key === 'Enter' || event.key === ' ') {
var _key$getState2;
const pluginState = ((_key$getState2 = key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.sort) || {};
toggleSort(view, event, pluginState);
}
},
click: (view, event) => {
var _key$getState3;
const pluginState = ((_key$getState3 = key.getState(view.state)) === null || _key$getState3 === void 0 ? void 0 : _key$getState3.sort) || {};
toggleSort(view, event, pluginState);
}
},
decorations(state) {
var _key$getState4;
const decs = ((_key$getState4 = key.getState(state)) === null || _key$getState4 === void 0 ? void 0 : _key$getState4.decorations) || DecorationSet.empty;
return decs;
}
}
});
};