UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

535 lines • 23.4 kB
import { Plugin, PluginKey, tableEditing, CellSelection, Selection, TableMap, Slice, Decoration, DecorationSet, TextSelection, } from '../../prosemirror'; import keymapHandler from './keymap'; import * as tableBaseCommands from '../../prosemirror/prosemirror-tables'; import { getColumnPos, getRowPos, getTablePos, getSelectedColumn, getSelectedRow, containsTableHeader, } from './utils'; import { analyticsService } from '../../analytics'; var TableState = (function () { function TableState(state, pluginConfig) { if (pluginConfig === void 0) { pluginConfig = {}; } var _this = this; this.editorFocused = false; this.toolbarFocused = false; this.tableHidden = false; this.tableDisabled = false; this.tableActive = false; this.domEvent = false; this.hoveredCells = []; this.isHeaderRowRequired = false; this.changeHandlers = []; this.insertColumn = function (column) { if (_this.tableNode) { var map = TableMap.get(_this.tableNode); var dispatch = _this.view.dispatch; // last column if (column === map.width) { // to add a column we need to move the cursor to an appropriate cell first var prevColPos = map.positionAt(0, column - 1, _this.tableNode); _this.moveCursorTo(prevColPos); tableBaseCommands.addColumnAfter(_this.view.state, dispatch); // then we move the cursor to the newly created cell var nextPos = TableMap.get(_this.tableNode).positionAt(0, column, _this.tableNode); _this.moveCursorTo(nextPos); } else { var pos = map.positionAt(0, column, _this.tableNode); _this.moveCursorTo(pos); tableBaseCommands.addColumnBefore(_this.view.state, dispatch); _this.moveCursorTo(pos); } analyticsService.trackEvent('atlassian.editor.format.table.column.button'); } }; this.insertRow = function (row) { if (_this.tableNode) { var map = TableMap.get(_this.tableNode); var dispatch = _this.view.dispatch; // last row if (row === map.height) { var prevRowPos = map.positionAt(row - 1, 0, _this.tableNode); _this.moveCursorTo(prevRowPos); tableBaseCommands.addRowAfter(_this.view.state, dispatch); var nextPos = TableMap.get(_this.tableNode).positionAt(row, 0, _this.tableNode); _this.moveCursorTo(nextPos); } else { var pos = map.positionAt(row, 0, _this.tableNode); _this.moveCursorTo(pos); tableBaseCommands.addRowBefore(_this.view.state, dispatch); _this.moveCursorTo(pos); } analyticsService.trackEvent('atlassian.editor.format.table.row.button'); } }; this.remove = function () { if (!_this.cellSelection) { return; } var _a = _this.view, state = _a.state, dispatch = _a.dispatch; var isRowSelected = _this.cellSelection.isRowSelection(); var isColumnSelected = _this.cellSelection.isColSelection(); // the whole table if (isRowSelected && isColumnSelected) { tableBaseCommands.deleteTable(state, dispatch); _this.focusEditor(); analyticsService.trackEvent('atlassian.editor.format.table.delete.button'); } else if (isColumnSelected) { analyticsService.trackEvent('atlassian.editor.format.table.delete_column.button'); // move the cursor in the column to the left of the deleted column(s) var map = TableMap.get(_this.tableNode); var _b = getSelectedColumn(_this.view.state, map), anchor = _b.anchor, head = _b.head; var column = Math.min(anchor, head); var nextPos = map.positionAt(0, column > 0 ? column - 1 : 0, _this.tableNode); tableBaseCommands.deleteColumn(state, dispatch); _this.moveCursorTo(nextPos); } else if (isRowSelected) { var tableHeader = _this.view.state.schema.nodes.tableHeader; var cell = _this.getCurrentCell(); var event_1 = cell && cell.type === tableHeader ? 'delete_header_row' : 'delete_row'; analyticsService.trackEvent("atlassian.editor.format.table." + event_1 + ".button"); var headerRowSelected = _this.isHeaderRowSelected(); // move the cursor to the beginning of the next row, or prev row if deleted row was the last row var _c = getSelectedRow(_this.view.state), anchor = _c.anchor, head = _c.head; var map = TableMap.get(_this.tableNode); var minRow = Math.min(anchor, head); var maxRow = Math.max(anchor, head); var isRemovingLastRow = maxRow === (map.height - 1); tableBaseCommands.deleteRow(state, dispatch); if (headerRowSelected && _this.isHeaderRowRequired) { _this.convertFirstRowToHeader(); } var nextPos = map.positionAt(isRemovingLastRow ? minRow - 1 : minRow, 0, _this.tableNode); _this.moveCursorTo(nextPos); } else { // replace selected cells with empty cells _this.emptySelectedCells(); _this.moveCursorInsideTableTo(state.selection.from); analyticsService.trackEvent('atlassian.editor.format.table.delete_content.button'); } }; this.convertFirstRowToHeader = function () { _this.selectRow(0); var _a = _this.view, state = _a.state, dispatch = _a.dispatch; tableBaseCommands.toggleHeaderRow(state, dispatch); }; this.selectColumn = function (column) { if (_this.tableNode) { var _a = getColumnPos(column, _this.tableNode), from = _a.from, to = _a.to; _this.createCellSelection(from, to); } }; this.selectRow = function (row) { if (_this.tableNode) { var _a = getRowPos(row, _this.tableNode), from = _a.from, to = _a.to; _this.createCellSelection(from, to); } }; this.selectTable = function () { if (_this.tableNode) { var _a = getTablePos(_this.tableNode), from = _a.from, to = _a.to; _this.createCellSelection(from, to); } }; this.hoverColumn = function (column) { if (_this.tableNode) { var _a = getColumnPos(column, _this.tableNode), from = _a.from, to = _a.to; _this.createHoverSelection(from, to); } }; this.hoverRow = function (row) { if (_this.tableNode) { var _a = getRowPos(row, _this.tableNode), from = _a.from, to = _a.to; _this.createHoverSelection(from, to); } }; this.hoverTable = function () { if (_this.tableNode) { var _a = getTablePos(_this.tableNode), from = _a.from, to = _a.to; _this.createHoverSelection(from, to); } }; this.resetHoverSelection = function () { _this.hoveredCells = []; _this.view.dispatch(_this.view.state.tr); }; this.isColumnSelected = function (column) { if (_this.tableNode && _this.cellSelection) { var map = TableMap.get(_this.tableNode); var start = _this.cellSelection.$anchorCell.start(-1); var anchor = map.colCount(_this.cellSelection.$anchorCell.pos - start); var head = map.colCount(_this.cellSelection.$headCell.pos - start); return (_this.cellSelection.isColSelection() && (column <= Math.max(anchor, head) && column >= Math.min(anchor, head))); } return false; }; this.isRowSelected = function (row) { if (_this.cellSelection) { var anchor = _this.cellSelection.$anchorCell.index(-1); var head = _this.cellSelection.$headCell.index(-1); return (_this.cellSelection.isRowSelection() && (row <= Math.max(anchor, head) && row >= Math.min(anchor, head))); } return false; }; this.isHeaderRowSelected = function () { if (_this.cellSelection && _this.cellSelection.isRowSelection()) { var $from = _this.view.state.selection.$from; var tableHeader = _this.view.state.schema.nodes.tableHeader; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === tableHeader) { return true; } } } return false; }; this.isTableSelected = function () { if (_this.cellSelection) { return _this.cellSelection.isColSelection() && _this.cellSelection.isRowSelection(); } return false; }; this.isRequiredToAddHeader = function () { return _this.isHeaderRowRequired; }; this.addHeaderToTableNodes = function (slice, selectionStart) { var table = _this.view.state.schema.nodes.table; slice.content.forEach(function (node, offset) { if (node.type === table && !containsTableHeader(_this.view, node)) { var _a = _this.view, state = _a.state, dispatch = _a.dispatch; var tr = state.tr, doc = state.doc; var $anchor = doc.resolve(selectionStart + offset); dispatch(tr.setSelection(new TextSelection($anchor))); _this.convertFirstRowToHeader(); } }); }; this.changeHandlers = []; var _a = state.schema.nodes, table = _a.table, tableCell = _a.tableCell, tableRow = _a.tableRow, tableHeader = _a.tableHeader; this.tableHidden = !table || !tableCell || !tableRow || !tableHeader; this.isHeaderRowRequired = pluginConfig.isHeaderRowRequired || false; } TableState.prototype.subscribe = function (cb) { this.changeHandlers.push(cb); cb(this); }; TableState.prototype.unsubscribe = function (cb) { this.changeHandlers = this.changeHandlers.filter(function (ch) { return ch !== cb; }); }; TableState.prototype.updateEditorFocused = function (editorFocused) { this.editorFocused = editorFocused; }; TableState.prototype.updateToolbarFocused = function (toolbarFocused) { this.toolbarFocused = toolbarFocused; }; TableState.prototype.update = function (docView, domEvent) { if (domEvent === void 0) { domEvent = false; } var dirty = this.updateSelection(); var cellSelection = this.cellSelection; var tableElement = this.getTableElement(docView); if (domEvent && tableElement || tableElement !== this.tableElement) { this.tableElement = tableElement; this.domEvent = domEvent; dirty = true; } var tableNode = this.getTableNode(); if (tableNode !== this.tableNode) { this.tableNode = tableNode; dirty = true; } // show floating toolbar only when the whole row, column or table is selected var toolbarVisible = (cellSelection && (cellSelection.isColSelection() || cellSelection.isRowSelection()) ? true : false); var cellElement = toolbarVisible ? this.getFirstSelectedCellElement(docView) : undefined; if (cellElement !== this.cellElement) { this.cellElement = cellElement; dirty = true; } var tableActive = this.editorFocused && !!tableElement; if (tableActive !== this.tableActive) { this.tableActive = tableActive; dirty = true; } var tableDisabled = !this.canInsertTable(); if (tableDisabled !== this.tableDisabled) { this.tableDisabled = tableDisabled; dirty = true; } if (dirty) { this.triggerOnChange(); } }; TableState.prototype.setView = function (view) { this.view = view; }; TableState.prototype.tableStartPos = function () { var $from = this.view.state.selection.$from; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === this.view.state.schema.nodes.table) { return $from.start(i); } } }; TableState.prototype.closeFloatingToolbar = function () { this.clearSelection(); this.triggerOnChange(); }; TableState.prototype.getCurrentCellStartPos = function () { var $from = this.view.state.selection.$from; var _a = this.view.state.schema.nodes, tableCell = _a.tableCell, tableHeader = _a.tableHeader; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === tableCell || node.type === tableHeader) { return $from.start(i); } } }; TableState.prototype.getCurrentCell = function () { var $from = this.view.state.selection.$from; var _a = this.view.state.schema.nodes, tableCell = _a.tableCell, tableHeader = _a.tableHeader; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === tableCell || node.type === tableHeader) { return node; } } }; TableState.prototype.createHoverSelection = function (from, to) { var _this = this; if (!this.tableNode) { return; } var offset = this.tableStartPos(); if (offset) { var state_1 = this.view.state; var map = TableMap.get(this.tableNode); var cells = map.cellsInRect(map.rectBetween(from, to)); cells.forEach(function (cellPos) { var pos = cellPos + offset; var node = state_1.doc.nodeAt(pos); if (node) { _this.hoveredCells.push({ node: node, pos: pos }); } }); // trigger state change to be able to pick it up in the decorations handler this.view.dispatch(state_1.tr); } }; TableState.prototype.getTableElement = function (docView) { var offset = this.tableStartPos(); if (offset) { var node = docView.domFromPos(offset).node; if (node) { return node.parentNode; } } }; TableState.prototype.getFirstSelectedCellElement = function (docView) { var offset = this.firstSelectedCellStartPos(); if (offset) { var node = docView.domFromPos(offset).node; if (node) { return node; } } }; TableState.prototype.firstSelectedCellStartPos = function () { if (!this.tableNode) { return; } var offset = this.tableStartPos(); if (offset) { var state = this.view.state; var _a = state.selection, $anchorCell = _a.$anchorCell, $headCell = _a.$headCell; var _b = state.schema.nodes, tableCell = _b.tableCell, tableHeader = _b.tableHeader; var map = TableMap.get(this.tableNode); var start = $anchorCell.start(-1); // array of selected cells positions var cells = map.cellsInRect(map.rectBetween($anchorCell.pos - start, $headCell.pos - start)); // first selected cell position var firstCellPos = cells[0] + offset + 1; var $from = state.doc.resolve(firstCellPos); for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === tableCell || node.type === tableHeader) { return $from.start(i); } } } }; TableState.prototype.getTableNode = function () { var $from = this.view.state.selection.$from; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); if (node.type === this.view.state.schema.nodes.table) { return node; } } }; TableState.prototype.triggerOnChange = function () { var _this = this; this.changeHandlers.forEach(function (cb) { return cb(_this); }); }; TableState.prototype.createCellSelection = function (from, to) { var state = this.view.state; // here "from" and "to" params are table-relative positions, therefore we add table offset var offset = this.tableStartPos(); if (offset) { var $anchor = state.doc.resolve(from + offset); var $head = state.doc.resolve(to + offset); this.view.dispatch(this.view.state.tr.setSelection(new CellSelection($anchor, $head))); } }; // we keep track of selection changes because // 1) we want to mark toolbar buttons as active when the whole row/col is selected // 2) we want to drop selection if editor looses focus TableState.prototype.updateSelection = function () { var selection = this.view.state.selection; var dirty = false; if (selection instanceof CellSelection) { if (selection !== this.cellSelection) { this.cellSelection = selection; dirty = true; } // drop selection if editor looses focus if (!this.editorFocused) { this.clearSelection(); } } else if (this.cellSelection) { this.cellSelection = undefined; dirty = true; } return dirty; }; TableState.prototype.clearSelection = function () { var state = this.view.state; this.cellElement = undefined; this.view.dispatch(state.tr.setSelection(Selection.near(state.selection.$from))); }; TableState.prototype.canInsertTable = function () { var state = this.view.state; var _a = state.selection, $from = _a.$from, to = _a.to; var code = state.schema.marks.code; for (var i = $from.depth; i > 0; i--) { var node = $from.node(i); // inline code and codeBlock are excluded if (node.type === state.schema.nodes.codeBlock || (code && state.doc.rangeHasMark($from.pos, to, code))) { return false; } } return true; }; TableState.prototype.emptySelectedCells = function () { if (!this.cellSelection) { return; } var _a = this.view.state, tr = _a.tr, schema = _a.schema; var emptyCell = schema.nodes.tableCell.createAndFill().content; this.cellSelection.forEachCell(function (cell, pos) { if (!cell.content.eq(emptyCell)) { var slice = new Slice(emptyCell, 0, 0); tr.replace(tr.mapping.map(pos + 1), tr.mapping.map(pos + cell.nodeSize - 1), slice); } }); if (tr.docChanged) { this.view.dispatch(tr); } }; TableState.prototype.focusEditor = function () { if (!this.view.hasFocus()) { this.view.focus(); } }; TableState.prototype.moveCursorInsideTableTo = function (pos) { this.focusEditor(); var tr = this.view.state.tr; tr.setSelection(Selection.near(tr.doc.resolve(pos))); this.view.dispatch(tr); }; TableState.prototype.moveCursorTo = function (pos) { var offset = this.tableStartPos(); if (offset) { this.moveCursorInsideTableTo(pos + offset); } }; return TableState; }()); export { TableState }; export var stateKey = new PluginKey('tablePlugin'); export var plugin = function (pluginConfig) { return new Plugin({ state: { init: function (config, state) { return new TableState(state, pluginConfig); }, apply: function (tr, pluginState, oldState, newState) { var stored = tr.getMeta(stateKey); if (stored) { pluginState.update(stored.docView, stored.domEvent); } return pluginState; } }, key: stateKey, view: function (editorView) { var pluginState = stateKey.getState(editorView.state); pluginState.setView(editorView); pluginState.update(editorView.docView); pluginState.keymapHandler = keymapHandler(pluginState); return { update: function (view, prevState) { pluginState.update(view.docView); } }; }, props: { decorations: function (state) { var pluginState = stateKey.getState(state); if (!pluginState.hoveredCells.length) { return; } var cells = pluginState.hoveredCells.map(function (cell) { return Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: 'hoveredCell' }); }); return DecorationSet.create(state.doc, cells); }, handleKeyDown: function (view, event) { return stateKey.getState(view.state).keymapHandler(view, event); }, handleClick: function (view, pos, event) { stateKey.getState(view.state).update(view.docView, true); return false; }, onFocus: function (view, event) { var pluginState = stateKey.getState(view.state); pluginState.updateEditorFocused(true); pluginState.update(view.docView, true); }, onBlur: function (view, event) { var pluginState = stateKey.getState(view.state); if (pluginState.toolbarFocused) { pluginState.updateToolbarFocused(false); } else { pluginState.updateEditorFocused(false); pluginState.update(view.docView, true); } pluginState.resetHoverSelection(); }, } }); }; var plugins = function (pluginConfig) { return [plugin(pluginConfig), tableEditing()].filter(function (plugin) { return !!plugin; }); }; export default plugins; // Disable inline table editing and resizing controls in Firefox // https://github.com/ProseMirror/prosemirror/issues/432 setTimeout(function () { document.execCommand('enableObjectResizing', false, 'false'); document.execCommand('enableInlineTableEditing', false, 'false'); }); //# sourceMappingURL=index.js.map