UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

257 lines (248 loc) 11.5 kB
/** * 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-next'; 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) => { 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; } } }); };