@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
535 lines • 23.4 kB
JavaScript
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