'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var prosemirrorState = require('prosemirror-state'); var prosemirrorModel = require('prosemirror-model'); var prosemirrorKeymap = require('prosemirror-keymap'); var prosemirrorView = require('prosemirror-view'); var prosemirrorTransform = require('prosemirror-transform'); // Because working with row and column-spanning cells is not quite // trivial, this code builds up a descriptive structure for a given // table node. The structures are cached with the (persistent) table // nodes as key, so that they only have to be recomputed when the // content of the table changes. // // This does mean that they have to store table-relative, not // document-relative positions. So code that uses them will typically // compute the start position of the table and offset positions passed // to or gotten from this structure by that amount. let readFromCache, addToCache; // Prefer using a weak map to cache table maps. Fall back on a // fixed-size cache if that's not supported. if (typeof WeakMap != 'undefined') { // eslint-disable-next-line let cache = new WeakMap(); readFromCache = key => cache.get(key); addToCache = (key, value) => { cache.set(key, value); return value; }; } else { let cache = [], cacheSize = 10, cachePos = 0; readFromCache = key => { for (let i = 0; i < cache.length; i += 2) if (cache[i] == key) return cache[i + 1]; }; addToCache = (key, value) => { if (cachePos == cacheSize) cachePos = 0; cache[cachePos++] = key; return cache[cachePos++] = value; }; } class Rect { constructor(left, top, right, bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } } // ::- A table map describes the structore of a given table. To avoid // recomputing them all the time, they are cached per table node. To // be able to do that, positions saved in the map are relative to the // start of the table, rather than the start of the document. class TableMap { constructor(width, height, map, problems) { // :: number The width of the table this.width = width; // :: number The table's height this.height = height; // :: [number] A width * height array with the start position of // the cell covering that part of the table in each slot this.map = map; // An optional array of problems (cell overlap or non-rectangular // shape) for the table, used by the table normalizer. this.problems = problems; } // :: (number) → Rect // Find the dimensions of the cell at the given position. findCell(pos) { for (let i = 0; i < this.map.length; i++) { let curPos = this.map[i]; if (curPos != pos) continue; let left = i % this.width, top = i / this.width | 0; let right = left + 1, bottom = top + 1; for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) right++; for (let j = 1; bottom < this.height && this.map[i + this.width * j] == curPos; j++) bottom++; return new Rect(left, top, right, bottom); } throw new RangeError('No cell with offset ' + pos + ' found'); } // :: (number) → number // Find the left side of the cell at the given position. colCount(pos) { for (let i = 0; i < this.map.length; i++) if (this.map[i] == pos) return i % this.width; throw new RangeError('No cell with offset ' + pos + ' found'); } // :: (number, string, number) → ?number // Find the next cell in the given direction, starting from the cell // at `pos`, if any. nextCell(pos, axis, dir) { let { left, right, top, bottom } = this.findCell(pos); if (axis == 'horiz') { if (dir < 0 ? left == 0 : right == this.width) return null; return this.map[top * this.width + (dir < 0 ? left - 1 : right)]; } else { if (dir < 0 ? top == 0 : bottom == this.height) return null; return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)]; } } // :: (number, number) → Rect // Get the rectangle spanning the two given cells. rectBetween(a, b) { let { left: leftA, right: rightA, top: topA, bottom: bottomA } = this.findCell(a); let { left: leftB, right: rightB, top: topB, bottom: bottomB } = this.findCell(b); return new Rect(Math.min(leftA, leftB), Math.min(topA, topB), Math.max(rightA, rightB), Math.max(bottomA, bottomB)); } // :: (Rect) → [number] // Return the position of all cells that have the top left corner in // the given rectangle. cellsInRect(rect) { let result = [], seen = {}; for (let row = rect.top; row < rect.bottom; row++) { for (let col = rect.left; col < rect.right; col++) { let index = row * this.width + col, pos = this.map[index]; if (seen[pos]) continue; seen[pos] = true; if ((col != rect.left || !col || this.map[index - 1] != pos) && (row != rect.top || !row || this.map[index - this.width] != pos)) result.push(pos); } } return result; } // :: (number, number, Node) → number // Return the position at which the cell at the given row and column // starts, or would start, if a cell started there. positionAt(row, col, table) { for (let i = 0, rowStart = 0;; i++) { let rowEnd = rowStart + table.child(i).nodeSize; if (i == row) { let index = col + row * this.width, rowEndIndex = (row + 1) * this.width; // Skip past cells from previous rows (via rowspan) while (index < rowEndIndex && this.map[index] < rowStart) index++; return index == rowEndIndex ? rowEnd - 1 : this.map[index]; } rowStart = rowEnd; } } // :: (Node) → TableMap // Find the table map for the given table node. static get(table) { return readFromCache(table) || addToCache(table, computeMap(table)); } } // Compute a table map. function computeMap(table) { if (table.type.spec.tableRole != 'table') throw new RangeError('Not a table node: ' + table.type.name); let width = findWidth(table), height = table.childCount; let map = [], mapPos = 0, problems = null, colWidths = []; for (let i = 0, e = width * height; i < e; i++) map[i] = 0; for (let row = 0, pos = 0; row < height; row++) { let rowNode = table.child(row); pos++; for (let i = 0;; i++) { while (mapPos < map.length && map[mapPos] != 0) mapPos++; if (i == rowNode.childCount) break; let cellNode = rowNode.child(i), { colspan, rowspan, colwidth } = cellNode.attrs; for (let h = 0; h < rowspan; h++) { if (h + row >= height) { (problems || (problems = [])).push({ type: 'overlong_rowspan', pos, n: rowspan - h }); break; } let start = mapPos + h * width; for (let w = 0; w < colspan; w++) { if (map[start + w] == 0) map[start + w] = pos;else (problems || (problems = [])).push({ type: 'collision', row, pos, n: colspan - w }); let colW = colwidth && colwidth[w]; if (colW) { let widthIndex = (start + w) % width * 2, prev = colWidths[widthIndex]; if (prev == null || prev != colW && colWidths[widthIndex + 1] == 1) { colWidths[widthIndex] = colW; colWidths[widthIndex + 1] = 1; } else if (prev == colW) { colWidths[widthIndex + 1]++; } } } } mapPos += colspan; pos += cellNode.nodeSize; } let expectedPos = (row + 1) * width, missing = 0; while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++; if (missing) (problems || (problems = [])).push({ type: 'missing', row, n: missing }); pos++; } let tableMap = new TableMap(width, height, map, problems), badWidths = false; // For columns that have defined widths, but whose widths disagree // between rows, fix up the cells whose width doesn't match the // computed one. for (let i = 0; !badWidths && i < colWidths.length; i += 2) if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true; if (badWidths) findBadColWidths(tableMap, colWidths, table); return tableMap; } function findWidth(table) { let width = -1, hasRowSpan = false; for (let row = 0; row < table.childCount; row++) { let rowNode = table.child(row), rowWidth = 0; if (hasRowSpan) for (let j = 0; j < row; j++) { let prevRow = table.child(j); for (let i = 0; i < prevRow.childCount; i++) { let cell = prevRow.child(i); if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan; } } for (let i = 0; i < rowNode.childCount; i++) { let cell = rowNode.child(i); rowWidth += cell.attrs.colspan; if (cell.attrs.rowspan > 1) hasRowSpan = true; } if (width == -1) width = rowWidth;else if (width != rowWidth) width = Math.max(width, rowWidth); } return width; } function findBadColWidths(map, colWidths, table) { if (!map.problems) map.problems = []; for (let i = 0, seen = {}; i < map.map.length; i++) { let pos = map.map[i]; if (seen[pos]) continue; seen[pos] = true; let node = table.nodeAt(pos), updated = null; for (let j = 0; j < node.attrs.colspan; j++) { let col = (i + j) % map.width, colWidth = colWidths[col * 2]; if (colWidth != null && (!node.attrs.colwidth || node.attrs.colwidth[j] != colWidth)) (updated || (updated = freshColWidth(node.attrs)))[j] = colWidth; } if (updated) map.problems.unshift({ type: 'colwidth mismatch', pos, colwidth: updated }); } } function freshColWidth(attrs) { if (attrs.colwidth) return attrs.colwidth.slice(); let result = []; for (let i = 0; i < attrs.colspan; i++) result.push(0); return result; } // Helper for creating a schema that supports tables. function getCellAttrs(dom, extraAttrs) { let widthAttr = dom.getAttribute('data-colwidth'); let widths = widthAttr && /^\d+(,\d+)*$/.test(widthAttr) ? widthAttr.split(',').map(s => Number(s)) : null; let colspan = Number(dom.getAttribute('colspan') || 1); let result = { colspan, rowspan: Number(dom.getAttribute('rowspan') || 1), colwidth: widths && widths.length == colspan ? widths : null }; for (let prop in extraAttrs) { let getter = extraAttrs[prop].getFromDOM; let value = getter && getter(dom); if (value != null) result[prop] = value; } return result; } function setCellAttrs(node, extraAttrs) { let attrs = {}; if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan; if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan; if (node.attrs.colwidth) attrs['data-colwidth'] = node.attrs.colwidth.join(','); for (let prop in extraAttrs) { let setter = extraAttrs[prop].setDOMAttr; if (setter) setter(node.attrs[prop], attrs); } return attrs; } // :: (Object) → Object // // This function creates a set of [node // specs](http://prosemirror.net/docs/ref/#model.SchemaSpec.nodes) for // `table`, `table_row`, and `table_cell` nodes types as used by this // module. The result can then be added to the set of nodes when // creating a a schema. // // options::- The following options are understood: // // tableGroup:: ?string // A group name (something like `"block"`) to add to the table // node type. // // cellContent:: string // The content expression for table cells. // // cellAttributes:: ?Object // Additional attributes to add to cells. Maps attribute names to // objects with the following properties: // // default:: any // The attribute's default value. // // getFromDOM:: ?(dom.Node) → any // A function to read the attribute's value from a DOM node. // // setDOMAttr:: ?(value: any, attrs: Object) // A function to add the attribute's value to an attribute // object that's used to render the cell's DOM. function tableNodes(options) { let extraAttrs = options.cellAttributes || {}; let cellAttrs = { colspan: { default: 1 }, rowspan: { default: 1 }, colwidth: { default: null } }; for (let prop in extraAttrs) cellAttrs[prop] = { default: extraAttrs[prop].default }; return { table: { content: 'table_row+', tableRole: 'table', isolating: true, group: options.tableGroup, parseDOM: [{ tag: 'table' }], toDOM() { return ['table', ['tbody', 0]]; } }, table_row: { content: '(table_cell | table_header)*', tableRole: 'row', parseDOM: [{ tag: 'tr' }], toDOM() { return ['tr', 0]; } }, table_cell: { content: options.cellContent, attrs: cellAttrs, tableRole: 'cell', isolating: true, parseDOM: [{ tag: 'td', getAttrs: dom => getCellAttrs(dom, extraAttrs) }], toDOM(node) { return ['td', setCellAttrs(node, extraAttrs), 0]; } }, table_header: { content: options.cellContent, attrs: cellAttrs, tableRole: 'header_cell', isolating: true, parseDOM: [{ tag: 'th', getAttrs: dom => getCellAttrs(dom, extraAttrs) }], toDOM(node) { return ['th', setCellAttrs(node, extraAttrs), 0]; } } }; } function tableNodeTypes(schema) { let result = schema.cached.tableNodeTypes; if (!result) { result = schema.cached.tableNodeTypes = {}; for (let name in schema.nodes) { let type = schema.nodes[name], role = type.spec.tableRole; if (role) result[role] = type; } } return result; } // Various helper function for working with tables const key$1 = new prosemirrorState.PluginKey('selectingCells'); function cellAround($pos) { for (let d = $pos.depth - 1; d > 0; d--) if ($pos.node(d).type.spec.tableRole == 'row') return $pos.node(0).resolve($pos.before(d + 1)); return null; } function cellWrapping($pos) { for (let d = $pos.depth; d > 0; d--) { // Sometimes the cell can be in the same depth. const role = $pos.node(d).type.spec.tableRole; if (role === 'cell' || role === 'header_cell') return $pos.node(d); } return null; } function isInTable(state) { let $head = state.selection.$head; for (let d = $head.depth; d > 0; d--) if ($head.node(d).type.spec.tableRole == 'row') return true; return false; } function selectionCell(state) { let sel = state.selection; if (sel.$anchorCell) { return sel.$anchorCell.pos > sel.$headCell.pos ? sel.$anchorCell : sel.$headCell; } else if (sel.node && sel.node.type.spec.tableRole == 'cell') { return sel.$anchor; } return cellAround(sel.$head) || cellNear(sel.$head); } function cellNear($pos) { for (let after = $pos.nodeAfter, pos = $pos.pos; after; after = after.firstChild, pos++) { let role = after.type.spec.tableRole; if (role == 'cell' || role == 'header_cell') return $pos.doc.resolve(pos); } for (let before = $pos.nodeBefore, pos = $pos.pos; before; before = before.lastChild, pos--) { let role = before.type.spec.tableRole; if (role == 'cell' || role == 'header_cell') return $pos.doc.resolve(pos - before.nodeSize); } } function pointsAtCell($pos) { return $pos.parent.type.spec.tableRole == 'row' && $pos.nodeAfter; } function moveCellForward($pos) { return $pos.node(0).resolve($pos.pos + $pos.nodeAfter.nodeSize); } function inSameTable($a, $b) { return $a.depth == $b.depth && $a.pos >= $b.start(-1) && $a.pos <= $b.end(-1); } function findCell($pos) { return TableMap.get($pos.node(-1)).findCell($pos.pos - $pos.start(-1)); } function colCount($pos) { return TableMap.get($pos.node(-1)).colCount($pos.pos - $pos.start(-1)); } function nextCell($pos, axis, dir) { let start = $pos.start(-1), map = TableMap.get($pos.node(-1)); let moved = map.nextCell($pos.pos - start, axis, dir); return moved == null ? null : $pos.node(0).resolve(start + moved); } function setAttr(attrs, name, value) { let result = {}; for (let prop in attrs) result[prop] = attrs[prop]; result[name] = value; return result; } function removeColSpan(attrs, pos, n = 1) { let result = setAttr(attrs, 'colspan', attrs.colspan - n); if (result.colwidth) { result.colwidth = result.colwidth.slice(); result.colwidth.splice(pos, n); if (!result.colwidth.some(w => w > 0)) result.colwidth = null; } return result; } function addColSpan(attrs, pos, n = 1) { let result = setAttr(attrs, 'colspan', attrs.colspan + n); if (result.colwidth) { result.colwidth = result.colwidth.slice(); for (let i = 0; i < n; i++) result.colwidth.splice(pos, 0, 0); } return result; } function columnIsHeader(map, table, col) { let headerCell = tableNodeTypes(table.type.schema).header_cell; for (let row = 0; row < map.height; row++) if (table.nodeAt(map.map[col + row * map.width]).type != headerCell) return false; return true; } // This file defines a ProseMirror selection subclass that models // subclass that represents a cell selection spanning part of a table. // With the plugin enabled, these will be created when the user // selects across cells, and will be drawn by giving selected cells a // `selectedCell` CSS class. class CellSelection extends prosemirrorState.Selection { // :: (ResolvedPos, ?ResolvedPos) // A table selection is identified by its anchor and head cells. The // positions given to this constructor should point _before_ two // cells in the same table. They may be the same, to select a single // cell. constructor($anchorCell, $headCell = $anchorCell) { let table = $anchorCell.node(-1), map = TableMap.get(table), start = $anchorCell.start(-1); let rect = map.rectBetween($anchorCell.pos - start, $headCell.pos - start); let doc = $anchorCell.node(0); let cells = map.cellsInRect(rect).filter(p => p != $headCell.pos - start); // Make the head cell the first range, so that it counts as the // primary part of the selection cells.unshift($headCell.pos - start); let ranges = cells.map(pos => { let cell = table.nodeAt(pos), from = pos + start + 1; return new prosemirrorState.SelectionRange(doc.resolve(from), doc.resolve(from + cell.content.size)); }); super(ranges[0].$from, ranges[0].$to, ranges); // :: ResolvedPos // A resolved position pointing _in front of_ the anchor cell (the one // that doesn't move when extending the selection). this.$anchorCell = $anchorCell; // :: ResolvedPos // A resolved position pointing in front of the head cell (the one // moves when extending the selection). this.$headCell = $headCell; } map(doc, mapping) { let $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos)); let $headCell = doc.resolve(mapping.map(this.$headCell.pos)); if (pointsAtCell($anchorCell) && pointsAtCell($headCell) && inSameTable($anchorCell, $headCell)) { let tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1); if (tableChanged && this.isRowSelection()) return CellSelection.rowSelection($anchorCell, $headCell);else if (tableChanged && this.isColSelection()) return CellSelection.colSelection($anchorCell, $headCell);else return new CellSelection($anchorCell, $headCell); } return prosemirrorState.TextSelection.between($anchorCell, $headCell); } // :: () → Slice // Returns a rectangular slice of table rows containing the selected // cells. content() { let table = this.$anchorCell.node(-1), map = TableMap.get(table), start = this.$anchorCell.start(-1); let rect = map.rectBetween(this.$anchorCell.pos - start, this.$headCell.pos - start); let seen = {}, rows = []; for (let row = rect.top; row < rect.bottom; row++) { let rowContent = []; for (let index = row * map.width + rect.left, col = rect.left; col < rect.right; col++, index++) { let pos = map.map[index]; if (!seen[pos]) { seen[pos] = true; let cellRect = map.findCell(pos), cell = table.nodeAt(pos); let extraLeft = rect.left - cellRect.left, extraRight = cellRect.right - rect.right; if (extraLeft > 0 || extraRight > 0) { let attrs = cell.attrs; if (extraLeft > 0) attrs = removeColSpan(attrs, 0, extraLeft); if (extraRight > 0) attrs = removeColSpan(attrs, attrs.colspan - extraRight, extraRight); if (cellRect.left < rect.left) cell = cell.type.createAndFill(attrs);else cell = cell.type.create(attrs, cell.content); } if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) { let attrs = setAttr(cell.attrs, 'rowspan', Math.min(cellRect.bottom, rect.bottom) - Math.max(cellRect.top, rect.top)); if (cellRect.top < rect.top) cell = cell.type.createAndFill(attrs);else cell = cell.type.create(attrs, cell.content); } rowContent.push(cell); } } rows.push(table.child(row).copy(prosemirrorModel.Fragment.from(rowContent))); } const fragment = this.isColSelection() && this.isRowSelection() ? table : rows; return new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(fragment), 1, 1); } replace(tr, content = prosemirrorModel.Slice.empty) { let mapFrom = tr.steps.length, ranges = this.ranges; for (let i = 0; i < ranges.length; i++) { let { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom); tr.replace(mapping.map($from.pos), mapping.map($to.pos), i ? prosemirrorModel.Slice.empty : content); } let sel = prosemirrorState.Selection.findFrom(tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)), -1); if (sel) tr.setSelection(sel); } replaceWith(tr, node) { this.replace(tr, new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(node), 0, 0)); } forEachCell(f) { let table = this.$anchorCell.node(-1), map = TableMap.get(table), start = this.$anchorCell.start(-1); let cells = map.cellsInRect(map.rectBetween(this.$anchorCell.pos - start, this.$headCell.pos - start)); for (let i = 0; i < cells.length; i++) f(table.nodeAt(cells[i]), start + cells[i]); } // :: () → bool // True if this selection goes all the way from the top to the // bottom of the table. isColSelection() { let anchorTop = this.$anchorCell.index(-1), headTop = this.$headCell.index(-1); if (Math.min(anchorTop, headTop) > 0) return false; let anchorBot = anchorTop + this.$anchorCell.nodeAfter.attrs.rowspan, headBot = headTop + this.$headCell.nodeAfter.attrs.rowspan; return Math.max(anchorBot, headBot) == this.$headCell.node(-1).childCount; } // :: (ResolvedPos, ?ResolvedPos) → CellSelection // Returns the smallest column selection that covers the given anchor // and head cell. static colSelection($anchorCell, $headCell = $anchorCell) { let map = TableMap.get($anchorCell.node(-1)), start = $anchorCell.start(-1); let anchorRect = map.findCell($anchorCell.pos - start), headRect = map.findCell($headCell.pos - start); let doc = $anchorCell.node(0); if (anchorRect.top <= headRect.top) { if (anchorRect.top > 0) $anchorCell = doc.resolve(start + map.map[anchorRect.left]); if (headRect.bottom < map.height) $headCell = doc.resolve(start + map.map[map.width * (map.height - 1) + headRect.right - 1]); } else { if (headRect.top > 0) $headCell = doc.resolve(start + map.map[headRect.left]); if (anchorRect.bottom < map.height) $anchorCell = doc.resolve(start + map.map[map.width * (map.height - 1) + anchorRect.right - 1]); } return new CellSelection($anchorCell, $headCell); } // :: () → bool // True if this selection goes all the way from the left to the // right of the table. isRowSelection() { let map = TableMap.get(this.$anchorCell.node(-1)), start = this.$anchorCell.start(-1); let anchorLeft = map.colCount(this.$anchorCell.pos - start), headLeft = map.colCount(this.$headCell.pos - start); if (Math.min(anchorLeft, headLeft) > 0) return false; let anchorRight = anchorLeft + this.$anchorCell.nodeAfter.attrs.colspan, headRight = headLeft + this.$headCell.nodeAfter.attrs.colspan; return Math.max(anchorRight, headRight) == map.width; } eq(other) { return other instanceof CellSelection && other.$anchorCell.pos == this.$anchorCell.pos && other.$headCell.pos == this.$headCell.pos; } // :: (ResolvedPos, ?ResolvedPos) → CellSelection // Returns the smallest row selection that covers the given anchor // and head cell. static rowSelection($anchorCell, $headCell = $anchorCell) { let map = TableMap.get($anchorCell.node(-1)), start = $anchorCell.start(-1); let anchorRect = map.findCell($anchorCell.pos - start), headRect = map.findCell($headCell.pos - start); let doc = $anchorCell.node(0); if (anchorRect.left <= headRect.left) { if (anchorRect.left > 0) $anchorCell = doc.resolve(start + map.map[anchorRect.top * map.width]); if (headRect.right < map.width) $headCell = doc.resolve(start + map.map[map.width * (headRect.top + 1) - 1]); } else { if (headRect.left > 0) $headCell = doc.resolve(start + map.map[headRect.top * map.width]); if (anchorRect.right < map.width) $anchorCell = doc.resolve(start + map.map[map.width * (anchorRect.top + 1) - 1]); } return new CellSelection($anchorCell, $headCell); } toJSON() { return { type: 'cell', anchor: this.$anchorCell.pos, head: this.$headCell.pos }; } static fromJSON(doc, json) { return new CellSelection(doc.resolve(json.anchor), doc.resolve(json.head)); } // :: (Node, number, ?number) → CellSelection static create(doc, anchorCell, headCell = anchorCell) { return new CellSelection(doc.resolve(anchorCell), doc.resolve(headCell)); } getBookmark() { return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos); } } CellSelection.prototype.visible = false; prosemirrorState.Selection.jsonID('cell', CellSelection); class CellBookmark { constructor(anchor, head) { this.anchor = anchor; this.head = head; } map(mapping) { return new CellBookmark(mapping.map(this.anchor), mapping.map(this.head)); } resolve(doc) { let $anchorCell = doc.resolve(this.anchor), $headCell = doc.resolve(this.head); if ($anchorCell.parent.type.spec.tableRole == 'row' && $headCell.parent.type.spec.tableRole == 'row' && $anchorCell.index() < $anchorCell.parent.childCount && $headCell.index() < $headCell.parent.childCount && inSameTable($anchorCell, $headCell)) return new CellSelection($anchorCell, $headCell);else return prosemirrorState.Selection.near($headCell, 1); } } function drawCellSelection(state) { if (!(state.selection instanceof CellSelection)) return null; let cells = []; state.selection.forEachCell((node, pos) => { cells.push(prosemirrorView.Decoration.node(pos, pos + node.nodeSize, { class: 'selectedCell' })); }); return prosemirrorView.DecorationSet.create(state.doc, cells); } function isCellBoundarySelection({ $from, $to }) { if ($from.pos == $to.pos || $from.pos < $from.pos - 6) return false; // Cheap elimination let afterFrom = $from.pos, beforeTo = $to.pos, depth = $from.depth; for (; depth >= 0; depth--, afterFrom++) if ($from.after(depth + 1) < $from.end(depth)) break; for (let d = $to.depth; d >= 0; d--, beforeTo--) if ($to.before(d + 1) > $to.start(d)) break; return afterFrom == beforeTo && /row|table/.test($from.node(depth).type.spec.tableRole); } function isTextSelectionAcrossCells({ $from, $to }) { let fromCellBoundaryNode; let toCellBoundaryNode; for (let i = $from.depth; i > 0; i--) { let node = $from.node(i); if (node.type.spec.tableRole === 'cell' || node.type.spec.tableRole === 'header_cell') { fromCellBoundaryNode = node; break; } } for (let i = $to.depth; i > 0; i--) { let node = $to.node(i); if (node.type.spec.tableRole === 'cell' || node.type.spec.tableRole === 'header_cell') { toCellBoundaryNode = node; break; } } return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0; } function normalizeSelection(state, tr, allowTableNodeSelection) { let sel = (tr || state).selection, doc = (tr || state).doc, normalize, role; if (sel instanceof prosemirrorState.NodeSelection && (role = sel.node.type.spec.tableRole)) { if (role == 'cell' || role == 'header_cell') { normalize = CellSelection.create(doc, sel.from); } else if (role == 'row') { let $cell = doc.resolve(sel.from + 1); normalize = CellSelection.rowSelection($cell, $cell); } else if (!allowTableNodeSelection) { let map = TableMap.get(sel.node), start = sel.from + 1; let lastCell = start + map.map[map.width * map.height - 1]; normalize = CellSelection.create(doc, start + 1, lastCell); } } else if (sel instanceof prosemirrorState.TextSelection && isCellBoundarySelection(sel)) { normalize = prosemirrorState.TextSelection.create(doc, sel.from); } else if (sel instanceof prosemirrorState.TextSelection && isTextSelectionAcrossCells(sel)) { normalize = prosemirrorState.TextSelection.create(doc, sel.$from.start(), sel.$from.end()); } if (normalize) (tr || (tr = state.tr)).setSelection(normalize); return tr; } // Utilities used for copy/paste handling. // : (Slice) → ?{width: number, height: number, rows: [Fragment]} // Get a rectangular area of cells from a slice, or null if the outer // nodes of the slice aren't table cells or rows. function pastedCells(slice) { if (!slice.size) return null; let { content, openStart, openEnd } = slice; while (content.childCount == 1 && (openStart > 0 && openEnd > 0 || content.firstChild.type.spec.tableRole == 'table')) { openStart--; openEnd--; content = content.firstChild.content; } let first = content.firstChild, role = first.type.spec.tableRole; let schema = first.type.schema, rows = []; if (role == 'row') { for (let i = 0; i < content.childCount; i++) { let cells = content.child(i).content; let left = i ? 0 : Math.max(0, openStart - 1); let right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1); if (left || right) cells = fitSlice(tableNodeTypes(schema).row, new prosemirrorModel.Slice(cells, left, right)).content; rows.push(cells); } } else if (role == 'cell' || role == 'header_cell') { rows.push(openStart || openEnd ? fitSlice(tableNodeTypes(schema).row, new prosemirrorModel.Slice(content, openStart, openEnd)).content : content); } else { return null; } return ensureRectangular(schema, rows); } // : (Schema, [Fragment]) → {width: number, height: number, rows: [Fragment]} // Compute the width and height of a set of cells, and make sure each // row has the same number of cells. function ensureRectangular(schema, rows) { let widths = []; for (let i = 0; i < rows.length; i++) { let row = rows[i]; for (let j = row.childCount - 1; j >= 0; j--) { let { rowspan, colspan } = row.child(j).attrs; for (let r = i; r < i + rowspan; r++) widths[r] = (widths[r] || 0) + colspan; } } let width = 0; for (let r = 0; r < widths.length; r++) width = Math.max(width, widths[r]); for (let r = 0; r < widths.length; r++) { if (r >= rows.length) rows.push(prosemirrorModel.Fragment.empty); if (widths[r] < width) { let empty = tableNodeTypes(schema).cell.createAndFill(), cells = []; for (let i = widths[r]; i < width; i++) cells.push(empty); rows[r] = rows[r].append(prosemirrorModel.Fragment.from(cells)); } } return { height: rows.length, width, rows }; } function fitSlice(nodeType, slice) { let node = nodeType.createAndFill(); let tr = new prosemirrorTransform.Transform(node).replace(0, node.content.size, slice); return tr.doc; } // : ({width: number, height: number, rows: [Fragment]}, number, number) → {width: number, height: number, rows: [Fragment]} // Clip or extend (repeat) the given set of cells to cover the given // width and height. Will clip rowspan/colspan cells at the edges when // they stick out. function clipCells({ width, height, rows }, newWidth, newHeight) { if (width != newWidth) { let added = [], newRows = []; for (let row = 0; row < rows.length; row++) { let frag = rows[row], cells = []; for (let col = added[row] || 0, i = 0; col < newWidth; i++) { let cell = frag.child(i % frag.childCount); if (col + cell.attrs.colspan > newWidth) cell = cell.type.create(removeColSpan(cell.attrs, cell.attrs.colspan, col + cell.attrs.colspan - newWidth), cell.content); cells.push(cell); col += cell.attrs.colspan; for (let j = 1; j < cell.attrs.rowspan; j++) added[row + j] = (added[row + j] || 0) + cell.attrs.colspan; } newRows.push(prosemirrorModel.Fragment.from(cells)); } rows = newRows; width = newWidth; } if (height != newHeight) { let newRows = []; for (let row = 0, i = 0; row < newHeight; row++, i++) { let cells = [], source = rows[i % height]; for (let j = 0; j < source.childCount; j++) { let cell = source.child(j); if (row + cell.attrs.rowspan > newHeight) cell = cell.type.create(setAttr(cell.attrs, 'rowspan', Math.max(1, newHeight - cell.attrs.rowspan)), cell.content); cells.push(cell); } newRows.push(prosemirrorModel.Fragment.from(cells)); } rows = newRows; height = newHeight; } return { width, height, rows }; } // Make sure a table has at least the given width and height. Return // true if something was changed. function growTable(tr, map, table, start, width, height, mapFrom) { let schema = tr.doc.type.schema, types = tableNodeTypes(schema), empty, emptyHead; if (width > map.width) { for (let row = 0, rowEnd = 0; row < map.height; row++) { let rowNode = table.child(row); rowEnd += rowNode.nodeSize; let cells = [], add; if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell) add = empty || (empty = types.cell.createAndFill());else add = emptyHead || (emptyHead = types.header_cell.createAndFill()); for (let i = map.width; i < width; i++) cells.push(add); tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells); } } if (height > map.height) { let cells = []; for (let i = 0, start = (map.height - 1) * map.width; i < Math.max(map.width, width); i++) { let header = i >= map.width ? false : table.nodeAt(map.map[start + i]).type == types.header_cell; cells.push(header ? emptyHead || (emptyHead = types.header_cell.createAndFill()) : empty || (empty = types.cell.createAndFill())); } let emptyRow = types.row.create(null, prosemirrorModel.Fragment.from(cells)), rows = []; for (let i = map.height; i < height; i++) rows.push(emptyRow); tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows); } return !!(empty || emptyHead); } // Make sure the given line (left, top) to (right, top) doesn't cross // any rowspan cells by splitting cells that cross it. Return true if // something changed. function isolateHorizontal(tr, map, table, start, left, right, top, mapFrom) { if (top == 0 || top == map.height) return false; let found = false; for (let col = left; col < right; col++) { let index = top * map.width + col, pos = map.map[index]; if (map.map[index - map.width] == pos) { found = true; let cell = table.nodeAt(pos); let { top: cellTop, left: cellLeft } = map.findCell(pos); tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + start), null, setAttr(cell.attrs, 'rowspan', top - cellTop)); tr.insert(tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)), cell.type.createAndFill(setAttr(cell.attrs, 'rowspan', cellTop + cell.attrs.rowspan - top))); col += cell.attrs.colspan - 1; } } return found; } // Make sure the given line (left, top) to (left, bottom) doesn't // cross any colspan cells by splitting cells that cross it. Return // true if something changed. function isolateVertical(tr, map, table, start, top, bottom, left, mapFrom) { if (left == 0 || left == map.width) return false; let found = false; for (let row = top; row < bottom; row++) { let index = row * map.width + left, pos = map.map[index]; if (map.map[index - 1] == pos) { found = true; let cell = table.nodeAt(pos), cellLeft = map.colCount(pos); let updatePos = tr.mapping.slice(mapFrom).map(pos + start); tr.setNodeMarkup(updatePos, null, removeColSpan(cell.attrs, left - cellLeft, cell.attrs.colspan - (left - cellLeft))); tr.insert(updatePos + cell.nodeSize, cell.type.createAndFill(removeColSpan(cell.attrs, 0, left - cellLeft))); row += cell.attrs.rowspan - 1; } } return found; } // Insert the given set of cells (as returned by `pastedCells`) into a // table, at the position pointed at by rect. function insertCells(state, dispatch, tableStart, rect, cells) { let table = tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc, map = TableMap.get(table); let { top, left } = rect; let right = left + cells.width, bottom = top + cells.height; let tr = state.tr, mapFrom = 0; function recomp() { table = tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc; map = TableMap.get(table); mapFrom = tr.mapping.maps.length; } // Prepare the table to be large enough and not have any cells // crossing the boundaries of the rectangle that we want to // insert into. If anything about it changes, recompute the table // map so that subsequent operations can see the current shape. if (growTable(tr, map, table, tableStart, right, bottom, mapFrom)) recomp(); if (isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom)) recomp(); if (isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom)) recomp(); if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom)) recomp(); if (isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom)) recomp(); for (let row = top; row < bottom; row++) { let from = map.positionAt(row, left, table), to = map.positionAt(row, right, table); tr.replace(tr.mapping.slice(mapFrom).map(from + tableStart), tr.mapping.slice(mapFrom).map(to + tableStart), new prosemirrorModel.Slice(cells.rows[row - top], 0, 0)); } recomp(); tr.setSelection(new CellSelection(tr.doc.resolve(tableStart + map.positionAt(top, left, table)), tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table)))); dispatch(tr); } // This file defines a number of helpers for wiring up user input to const handleKeyDown = prosemirrorKeymap.keydownHandler({ ArrowLeft: arrow('horiz', -1), ArrowRight: arrow('horiz', 1), ArrowUp: arrow('vert', -1), ArrowDown: arrow('vert', 1), 'Shift-ArrowLeft': shiftArrow('horiz', -1), 'Shift-ArrowRight': shiftArrow('horiz', 1), 'Shift-ArrowUp': shiftArrow('vert', -1), 'Shift-ArrowDown': shiftArrow('vert', 1), Backspace: deleteCellSelection, 'Mod-Backspace': deleteCellSelection, Delete: deleteCellSelection, 'Mod-Delete': deleteCellSelection }); function maybeSetSelection(state, dispatch, selection) { if (selection.eq(state.selection)) return false; if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView()); return true; } function arrow(axis, dir) { return (state, dispatch, view) => { let sel = state.selection; if (sel instanceof CellSelection) { return maybeSetSelection(state, dispatch, prosemirrorState.Selection.near(sel.$headCell, dir)); } if (axis != 'horiz' && !sel.empty) return false; let end = atEndOfCell(view, axis, dir); if (end == null) return false; if (axis == 'horiz') { return maybeSetSelection(state, dispatch, prosemirrorState.Selection.near(state.doc.resolve(sel.head + dir), dir)); } else { let $cell = state.doc.resolve(end), $next = nextCell($cell, axis, dir), newSel; if ($next) newSel = prosemirrorState.Selection.near($next, 1);else if (dir < 0) newSel = prosemirrorState.Selection.near(state.doc.resolve($cell.before(-1)), -1);else newSel = prosemirrorState.Selection.near(state.doc.resolve($cell.after(-1)), 1); return maybeSetSelection(state, dispatch, newSel); } }; } function shiftArrow(axis, dir) { return (state, dispatch, view) => { let sel = state.selection; if (!(sel instanceof CellSelection)) { let end = atEndOfCell(view, axis, dir); if (end == null) return false; sel = new CellSelection(state.doc.resolve(end)); } let $head = nextCell(sel.$headCell, axis, dir); if (!$head) return false; return maybeSetSelection(state, dispatch, new CellSelection(sel.$anchorCell, $head)); }; } function deleteCellSelection(state, dispatch) { let sel = state.selection; if (!(sel instanceof CellSelection)) return false; if (dispatch) { let tr = state.tr, baseContent = tableNodeTypes(state.schema).cell.createAndFill().content; sel.forEachCell((cell, pos) => { if (!cell.content.eq(baseContent)) tr.replace(tr.mapping.map(pos + 1), tr.mapping.map(pos + cell.nodeSize - 1), new prosemirrorModel.Slice(baseContent, 0, 0)); }); if (tr.docChanged) dispatch(tr); } return true; } function handleTripleClick(view, pos) { let doc = view.state.doc, $cell = cellAround(doc.resolve(pos)); if (!$cell) return false; view.dispatch(view.state.tr.setSelection(new CellSelection($cell))); return true; } function handlePaste(view, _, slice) { if (!isInTable(view.state)) return false; let cells = pastedCells(slice), sel = view.state.selection; if (sel instanceof CellSelection) { if (!cells) cells = { width: 1, height: 1, rows: [prosemirrorModel.Fragment.from(fitSlice(tableNodeTypes(view.state.schema).cell, slice))] }; let table = sel.$anchorCell.node(-1), start = sel.$anchorCell.start(-1); let rect = TableMap.get(table).rectBetween(sel.$anchorCell.pos - start, sel.$headCell.pos - start); cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top); insertCells(view.state, view.dispatch, start, rect, cells); return true; } else if (cells) { let $cell = selectionCell(view.state), start = $cell.start(-1); insertCells(view.state, view.dispatch, start, TableMap.get($cell.node(-1)).findCell($cell.pos - start), cells); return true; } else { return false; } } function handleMouseDown$1(view, startEvent) { if (startEvent.ctrlKey || startEvent.metaKey) return; let startDOMCell = domInCell(view, startEvent.target), $anchor; if (startEvent.shiftKey && view.state.selection instanceof CellSelection) { // Adding to an existing cell selection setCellSelection(view.state.selection.$anchorCell, startEvent); startEvent.preventDefault(); } else if (startEvent.shiftKey && startDOMCell && ($anchor = cellAround(view.state.selection.$anchor)) != null && cellUnderMouse(view, startEvent).pos != $anchor.pos) { // Adding to a selection that starts in another cell (causing a // cell selection to be created). setCellSelection($anchor, startEvent); startEvent.preventDefault(); } else if (!startDOMCell) { // Not in a cell, let the default behavior happen. return; } // Create and dispatch a cell selection between the given anchor and // the position under the mouse. function setCellSelection($anchor, event) { let $head = cellUnderMouse(view, event); let starting = key$1.getState(view.state) == null; if (!$head || !inSameTable($anchor, $head)) { if (starting) $head = $anchor;else return; } let selection = new CellSelection($anchor, $head); if (starting || !view.state.selection.eq(selection)) { let tr = view.state.tr.setSelection(selection); if (starting) tr.setMeta(key$1, $anchor.pos); view.dispatch(tr); } } // Stop listening to mouse motion events. function stop() { view.root.removeEventListener('mouseup', stop); view.root.removeEventListener('dragstart', stop); view.root.removeEventListener('mousemove', move); if (key$1.getState(view.state) != null) view.dispatch(view.state.tr.setMeta(key$1, -1)); } function move(event) { let anchor = key$1.getState(view.state), $anchor; if (anchor != null) { // Continuing an existing cross-cell selection $anchor = view.state.doc.resolve(anchor); } else if (domInCell(view, event.target) != startDOMCell) { // Moving out of the initial cell -- start a new cell selection $anchor = cellUnderMouse(view, startEvent); if (!$anchor) return stop(); } if ($anchor) setCellSelection($anchor, event); } view.root.addEventListener('mouseup', stop); view.root.addEventListener('dragstart', stop); view.root.addEventListener('mousemove', move); } // Check whether the cursor is at the end of a cell (so that further // motion would move out of the cell) function atEndOfCell(view, axis, dir) { if (!(view.state.selection instanceof prosemirrorState.TextSelection)) return null; let { $head } = view.state.selection; for (let d = $head.depth - 1; d >= 0; d--) { let parent = $head.node(d), index = dir < 0 ? $head.index(d) : $head.indexAfter(d); if (index != (dir < 0 ? 0 : parent.childCount)) return null; if (parent.type.spec.tableRole == 'cell' || parent.type.spec.tableRole == 'header_cell') { let cellPos = $head.before(d); let dirStr = axis == 'vert' ? dir > 0 ? 'down' : 'up' : dir > 0 ? 'right' : 'left'; return view.endOfTextblock(dirStr) ? cellPos : null; } } return null; } function domInCell(view, dom) { for (; dom && dom != view.dom; dom = dom.parentNode) if (dom.nodeName == 'TD' || dom.nodeName == 'TH') return dom; } function cellUnderMouse(view, event) { let mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!mousePos) return null; return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null; } // This file defines helpers for normalizing tables, making sure no const fixTablesKey = new prosemirrorState.PluginKey('fix-tables'); // Helper for iterating through the nodes in a document that changed // compared to the given previous document. Useful for avoiding // duplicate work on each transaction. function changedDescendants(old, cur, offset, f) { let oldSize = old.childCount, curSize = cur.childCount; outer: for (let i = 0, j = 0; i < curSize; i++) { let child = cur.child(i); for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) { if (old.child(scan) == child) { j = scan + 1; offset += child.nodeSize; continue outer; } } f(child, offset); if (j < oldSize && old.child(j).sameMarkup(child)) changedDescendants(old.child(j), child, offset + 1, f);else child.nodesBetween(0, child.content.size, f, offset + 1); offset += child.nodeSize; } } // :: (EditorState, ?EditorState) → ?Transaction // Inspect all tables in the given state's document and return a // transaction that fixes them, if necessary. If `oldState` was // provided, that is assumed to hold a previous, known-good state, // which will be used to avoid re-scanning unchanged parts of the // document. function fixTables(state, oldState) { let tr, check = (node, pos) => { if (node.type.spec.tableRole == 'table') tr = fixTable(state, node, pos, tr); }; if (!oldState) state.doc.descendants(check);else if (oldState.doc != state.doc) changedDescendants(oldState.doc, state.doc, 0, check); return tr; } // : (EditorState, Node, number, ?Transaction) → ?Transaction // Fix the given table, if necessary. Will append to the transaction // it was given, if non-null, or create a new one if necessary. function fixTable(state, table, tablePos, tr) { let map = TableMap.get(table); if (!map.problems) return tr; if (!tr) tr = state.tr; // Track which rows we must add cells to, so that we can adjust that // when fixing collisions. let mustAdd = []; for (let i = 0; i < map.height; i++) mustAdd.push(0); for (let i = 0; i < map.problems.length; i++) { let prob = map.problems[i]; if (prob.type == 'collision') { let cell = table.nodeAt(prob.pos); for (let j = 0; j < cell.attrs.rowspan; j++) mustAdd[prob.row + j] += prob.n; tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, removeColSpan(cell.attrs, cell.attrs.colspan - prob.n, prob.n)); } else if (prob.type == 'missing') { mustAdd[prob.row] += prob.n; } else if (prob.type == 'overlong_rowspan') { let cell = table.nodeAt(prob.pos); tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, setAttr(cell.attrs, 'rowspan', cell.attrs.rowspan - prob.n)); } else if (prob.type == 'colwidth mismatch') { let cell = table.nodeAt(prob.pos); tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, setAttr(cell.attrs, 'colwidth', prob.colwidth)); } } let first, last; for (let i = 0; i < mustAdd.length; i++) if (mustAdd[i]) { if (first == null) first = i; last = i; } // Add the necessary cells, using a heuristic for whether to add the // cells at the start or end of the rows (if it looks like a 'bite' // was taken out of the table, add cells at the start of the row // after the bite. Otherwise add them at the end). for (let i = 0, pos = tablePos + 1; i < map.height; i++) { let row = table.child(i); let end = pos + row.nodeSize; let add = mustAdd[i]; if (add > 0) { let tableNodeType = 'cell'; if (row.firstChild) { tableNodeType = row.firstChild.type.spec.tableRole; } let nodes = []; for (let j = 0; j < add; j++) nodes.push(tableNodeTypes(state.schema)[tableNodeType].createAndFill()); let side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1; tr.insert(tr.mapping.map(side), nodes); } pos = end; } return tr.setMeta(fixTablesKey, { fixTables: true }); } // This file defines a number of table-related commands. // map, table node, and table start offset to the object for // convenience. function selectedRect(state) { let sel = state.selection, $pos = selectionCell(state); let table = $pos.node(-1), tableStart = $pos.start(-1), map = TableMap.get(table); let rect; if (sel instanceof CellSelection) rect = map.rectBetween(sel.$anchorCell.pos - tableStart, sel.$headCell.pos - tableStart);else rect = map.findCell($pos.pos - tableStart); rect.tableStart = tableStart; rect.map = map; rect.table = table; return rect; } // Add a column at the given position in a table. function addColumn(tr, { map, tableStart, table }, col) { let refColumn = col > 0 ? -1 : 0; if (columnIsHeader(map, table, col + refColumn)) refColumn = col == 0 || col == map.width ? null : 0; for (let row = 0; row < map.height; row++) { let index = row * map.width + col; // If this position falls inside a col-spanning cell if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) { let pos = map.map[index], cell = table.nodeAt(pos); tr.setNodeMarkup(tr.mapping.map(tableStart + pos), null, addColSpan(cell.attrs, col - map.colCount(pos))); // Skip ahead if rowspan > 1 row += cell.attrs.rowspan - 1; } else { let type = refColumn == null ? tableNodeTypes(table.type.schema).cell : table.nodeAt(map.map[index + refColumn]).type; let pos = map.positionAt(row, col, table); tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill()); } } return tr; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Command to add a column before the column with the selection. function addColumnBefore(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state); dispatch(addColumn(state.tr, rect, rect.left)); } return true; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Command to add a column after the column with the selection. function addColumnAfter(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state); dispatch(addColumn(state.tr, rect, rect.right)); } return true; } function removeColumn(tr, { map, table, tableStart }, col) { let mapStart = tr.mapping.maps.length; for (let row = 0; row < map.height;) { let index = row * map.width + col, pos = map.map[index], cell = table.nodeAt(pos); // If this is part of a col-spanning cell if (col > 0 && map.map[index - 1] == pos || col < map.width - 1 && map.map[index + 1] == pos) { tr.setNodeMarkup(tr.mapping.slice(mapStart).map(tableStart + pos), null, removeColSpan(cell.attrs, col - map.colCount(pos))); } else { let start = tr.mapping.slice(mapStart).map(tableStart + pos); tr.delete(start, start + cell.nodeSize); } row += cell.attrs.rowspan; } } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Command function that removes the selected columns from a table. function deleteColumn(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state), tr = state.tr; if (rect.left == 0 && rect.right == rect.map.width) return false; for (let i = rect.right - 1;; i--) { removeColumn(tr, rect, i); if (i == rect.left) break; rect.table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc; rect.map = TableMap.get(rect.table); } dispatch(tr); } return true; } function rowIsHeader(map, table, row) { let headerCell = tableNodeTypes(table.type.schema).header_cell; for (let col = 0; col < map.width; col++) if (table.nodeAt(map.map[col + row * map.width]).type != headerCell) return false; return true; } function addRow(tr, { map, tableStart, table }, row) { let rowPos = tableStart; for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; let cells = [], refRow = row > 0 ? -1 : 0; if (rowIsHeader(map, table, row + refRow)) refRow = row == 0 || row == map.height ? null : 0; for (let col = 0, index = map.width * row; col < map.width; col++, index++) { // Covered by a rowspan cell if (row > 0 && row < map.height && map.map[index] == map.map[index - map.width]) { let pos = map.map[index], attrs = table.nodeAt(pos).attrs; tr.setNodeMarkup(tableStart + pos, null, setAttr(attrs, 'rowspan', attrs.rowspan + 1)); col += attrs.colspan - 1; } else { let type = refRow == null ? tableNodeTypes(table.type.schema).cell : table.nodeAt(map.map[index + refRow * map.width]).type; cells.push(type.createAndFill()); } } tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells)); return tr; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Add a table row before the selection. function addRowBefore(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state); dispatch(addRow(state.tr, rect, rect.top)); } return true; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Add a table row after the selection. function addRowAfter(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state); dispatch(addRow(state.tr, rect, rect.bottom)); } return true; } function removeRow(tr, { map, table, tableStart }, row) { let rowPos = 0; for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; let nextRow = rowPos + table.child(row).nodeSize; let mapFrom = tr.mapping.maps.length; tr.delete(rowPos + tableStart, nextRow + tableStart); for (let col = 0, index = row * map.width; col < map.width; col++, index++) { let pos = map.map[index]; if (row > 0 && pos == map.map[index - map.width]) { // If this cell starts in the row above, simply reduce its rowspan let attrs = table.nodeAt(pos).attrs; tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + tableStart), null, setAttr(attrs, 'rowspan', attrs.rowspan - 1)); col += attrs.colspan - 1; } else if (row < map.width && pos == map.map[index + map.width]) { // Else, if it continues in the row below, it has to be moved down let cell = table.nodeAt(pos); let copy = cell.type.create(setAttr(cell.attrs, 'rowspan', cell.attrs.rowspan - 1), cell.content); let newPos = map.positionAt(row + 1, col, table); tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy); col += cell.attrs.colspan - 1; } } } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Remove the selected rows from a table. function deleteRow(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let rect = selectedRect(state), tr = state.tr; if (rect.top == 0 && rect.bottom == rect.map.height) return false; for (let i = rect.bottom - 1;; i--) { removeRow(tr, rect, i); if (i == rect.top) break; rect.table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc; rect.map = TableMap.get(rect.table); } dispatch(tr); } return true; } function isEmpty(cell) { let c = cell.content; return c.childCount == 1 && c.firstChild.isTextblock && c.firstChild.childCount == 0; } function cellsOverlapRectangle({ width, height, map }, rect) { let indexTop = rect.top * width + rect.left, indexLeft = indexTop; let indexBottom = (rect.bottom - 1) * width + rect.left, indexRight = indexTop + (rect.right - rect.left - 1); for (let i = rect.top; i < rect.bottom; i++) { if (rect.left > 0 && map[indexLeft] == map[indexLeft - 1] || rect.right < width && map[indexRight] == map[indexRight + 1]) return true; indexLeft += width; indexRight += width; } for (let i = rect.left; i < rect.right; i++) { if (rect.top > 0 && map[indexTop] == map[indexTop - width] || rect.bottom < height && map[indexBottom] == map[indexBottom + width]) return true; indexTop++; indexBottom++; } return false; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Merge the selected cells into a single cell. Only available when // the selected cells' outline forms a rectangle. function mergeCells(state, dispatch) { let sel = state.selection; if (!(sel instanceof CellSelection) || sel.$anchorCell.pos == sel.$headCell.pos) return false; let rect = selectedRect(state), { map } = rect; if (cellsOverlapRectangle(map, rect)) return false; if (dispatch) { let tr = state.tr, seen = {}, content = prosemirrorModel.Fragment.empty, mergedPos, mergedCell; for (let row = rect.top; row < rect.bottom; row++) { for (let col = rect.left; col < rect.right; col++) { let cellPos = map.map[row * map.width + col], cell = rect.table.nodeAt(cellPos); if (seen[cellPos]) continue; seen[cellPos] = true; if (mergedPos == null) { mergedPos = cellPos; mergedCell = cell; } else { if (!isEmpty(cell)) content = content.append(cell.content); let mapped = tr.mapping.map(cellPos + rect.tableStart); tr.delete(mapped, mapped + cell.nodeSize); } } } tr.setNodeMarkup(mergedPos + rect.tableStart, null, setAttr(addColSpan(mergedCell.attrs, mergedCell.attrs.colspan, rect.right - rect.left - mergedCell.attrs.colspan), 'rowspan', rect.bottom - rect.top)); if (content.size) { let end = mergedPos + 1 + mergedCell.content.size; let start = isEmpty(mergedCell) ? mergedPos + 1 : end; tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content); } tr.setSelection(new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart))); dispatch(tr); } return true; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Split a selected cell, whose rowpan or colspan is greater than one, // into smaller cells. Use the first cell type for the new cells. function splitCell(state, dispatch) { const nodeTypes = tableNodeTypes(state.schema); return splitCellWithType(({ node }) => { return nodeTypes[node.type.spec.tableRole]; })(state, dispatch); } // :: (getCellType: ({ row: number, col: number, node: Node}) → NodeType) → (EditorState, dispatch: ?(tr: Transaction)) → bool // Split a selected cell, whose rowpan or colspan is greater than one, // into smaller cells with the cell type (th, td) returned by getType function. function splitCellWithType(getCellType) { return (state, dispatch) => { let sel = state.selection; let cellNode, cellPos; if (!(sel instanceof CellSelection)) { cellNode = cellWrapping(sel.$from); if (!cellNode) return false; cellPos = cellAround(sel.$from).pos; } else { if (sel.$anchorCell.pos != sel.$headCell.pos) return false; cellNode = sel.$anchorCell.nodeAfter; cellPos = sel.$anchorCell.pos; } if (cellNode.attrs.colspan == 1 && cellNode.attrs.rowspan == 1) { return false; } if (dispatch) { let baseAttrs = cellNode.attrs, attrs = [], colwidth = baseAttrs.colwidth; if (baseAttrs.rowspan > 1) baseAttrs = setAttr(baseAttrs, 'rowspan', 1); if (baseAttrs.colspan > 1) baseAttrs = setAttr(baseAttrs, 'colspan', 1); let rect = selectedRect(state), tr = state.tr; for (let i = 0; i < rect.right - rect.left; i++) attrs.push(colwidth ? setAttr(baseAttrs, 'colwidth', colwidth && colwidth[i] ? [colwidth[i]] : null) : baseAttrs); let lastCell; for (let row = rect.top; row < rect.bottom; row++) { let pos = rect.map.positionAt(row, rect.left, rect.table); if (row == rect.top) pos += cellNode.nodeSize; for (let col = rect.left, i = 0; col < rect.right; col++, i++) { if (col == rect.left && row == rect.top) continue; tr.insert(lastCell = tr.mapping.map(pos + rect.tableStart, 1), getCellType({ node: cellNode, row, col }).createAndFill(attrs[i])); } } tr.setNodeMarkup(cellPos, getCellType({ node: cellNode, row: rect.top, col: rect.left }), attrs[0]); if (sel instanceof CellSelection) tr.setSelection(new CellSelection(tr.doc.resolve(sel.$anchorCell.pos), lastCell && tr.doc.resolve(lastCell))); dispatch(tr); } return true; }; } // :: (string, any) → (EditorState, dispatch: ?(tr: Transaction)) → bool // Returns a command that sets the given attribute to the given value, // and is only available when the currently selected cell doesn't // already have that attribute set to that value. function setCellAttr(name, value) { return function (state, dispatch) { if (!isInTable(state)) return false; let $cell = selectionCell(state); if ($cell.nodeAfter.attrs[name] === value) return false; if (dispatch) { let tr = state.tr; if (state.selection instanceof CellSelection) state.selection.forEachCell((node, pos) => { if (node.attrs[name] !== value) tr.setNodeMarkup(pos, null, setAttr(node.attrs, name, value)); });else tr.setNodeMarkup($cell.pos, null, setAttr($cell.nodeAfter.attrs, name, value)); dispatch(tr); } return true; }; } function deprecated_toggleHeader(type) { return function (state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let types = tableNodeTypes(state.schema); let rect = selectedRect(state), tr = state.tr; let cells = rect.map.cellsInRect(type == 'column' ? new Rect(rect.left, 0, rect.right, rect.map.height) : type == 'row' ? new Rect(0, rect.top, rect.map.width, rect.bottom) : rect); let nodes = cells.map(pos => rect.table.nodeAt(pos)); for (let i = 0; i < cells.length; i++ // Remove headers, if any ) if (nodes[i].type == types.header_cell) tr.setNodeMarkup(rect.tableStart + cells[i], types.cell, nodes[i].attrs); if (tr.steps.length == 0) for (let i = 0; i < cells.length; i++ // No headers removed, add instead ) tr.setNodeMarkup(rect.tableStart + cells[i], types.header_cell, nodes[i].attrs); dispatch(tr); } return true; }; } function isHeaderEnabledByType(type, rect, types) { // Get cell positions for first row or first column const cellPositions = rect.map.cellsInRect({ left: 0, top: 0, right: type == 'row' ? rect.map.width : 1, bottom: type == 'column' ? rect.map.height : 1 }); for (let i = 0; i < cellPositions.length; i++) { const cell = rect.table.nodeAt(cellPositions[i]); if (cell && cell.type !== types.header_cell) { return false; } } return true; } // :: (string, ?{ useDeprecatedLogic: bool }) → (EditorState, dispatch: ?(tr: Transaction)) → bool // Toggles between row/column header and normal cells (Only applies to first row/column). // For deprecated behavior pass `useDeprecatedLogic` in options with true. function toggleHeader(type, options) { options = options || { useDeprecatedLogic: false }; if (options.useDeprecatedLogic) return deprecated_toggleHeader(type); return function (state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { let types = tableNodeTypes(state.schema); let rect = selectedRect(state), tr = state.tr; let isHeaderRowEnabled = isHeaderEnabledByType('row', rect, types); let isHeaderColumnEnabled = isHeaderEnabledByType('column', rect, types); let isHeaderEnabled = type === 'column' ? isHeaderRowEnabled : type === 'row' ? isHeaderColumnEnabled : false; let selectionStartsAt = isHeaderEnabled ? 1 : 0; let cellsRect = type == 'column' ? new Rect(0, selectionStartsAt, 1, rect.map.height) : type == 'row' ? new Rect(selectionStartsAt, 0, rect.map.width, 1) : rect; let newType = type == 'column' ? isHeaderColumnEnabled ? types.cell : types.header_cell : type == 'row' ? isHeaderRowEnabled ? types.cell : types.header_cell : types.cell; rect.map.cellsInRect(cellsRect).forEach(relativeCellPos => { const cellPos = relativeCellPos + rect.tableStart; const cell = tr.doc.nodeAt(cellPos); if (cell) { tr.setNodeMarkup(cellPos, newType, cell.attrs); } }); dispatch(tr); } return true; }; } // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Toggles whether the selected row contains header cells. let toggleHeaderRow = toggleHeader('row', { useDeprecatedLogic: true }); // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Toggles whether the selected column contains header cells. let toggleHeaderColumn = toggleHeader('column', { useDeprecatedLogic: true }); // :: (EditorState, dispatch: ?(tr: Transaction)) → bool // Toggles whether the selected cells are header cells. let toggleHeaderCell = toggleHeader('cell', { useDeprecatedLogic: true }); function findNextCell($cell, dir) { if (dir < 0) { let before = $cell.nodeBefore; if (before) return $cell.pos - before.nodeSize; for (let row = $cell.index(-1) - 1, rowEnd = $cell.before(); row >= 0; row--) { let rowNode = $cell.node(-1).child(row); if (rowNode.childCount) return rowEnd - 1 - rowNode.lastChild.nodeSize; rowEnd -= rowNode.nodeSize; } } else { if ($cell.index() < $cell.parent.childCount - 1) return $cell.pos + $cell.nodeAfter.nodeSize; let table = $cell.node(-1); for (let row = $cell.indexAfter(-1), rowStart = $cell.after(); row < table.childCount; row++) { let rowNode = table.child(row); if (rowNode.childCount) return rowStart + 1; rowStart += rowNode.nodeSize; } } } // :: (number) → (EditorState, dispatch: ?(tr: Transaction)) → bool // Returns a command for selecting the next (direction=1) or previous // (direction=-1) cell in a table. function goToNextCell(direction) { return function (state, dispatch) { if (!isInTable(state)) return false; let cell = findNextCell(selectionCell(state), direction); if (cell == null) return; if (dispatch) { let $cell = state.doc.resolve(cell); dispatch(state.tr.setSelection(prosemirrorState.TextSelection.between($cell, moveCellForward($cell))).scrollIntoView()); } return true; }; } // :: (EditorState, ?(tr: Transaction)) → bool // Deletes the table around the selection, if any. function deleteTable(state, dispatch) { let $pos = state.selection.$anchor; for (let d = $pos.depth; d > 0; d--) { let node = $pos.node(d); if (node.type.spec.tableRole == 'table') { if (dispatch) dispatch(state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView()); return true; } } return false; } class TableView { constructor(node, cellMinWidth) { this.node = node; this.cellMinWidth = cellMinWidth; this.dom = document.createElement('div'); this.dom.className = 'tableWrapper'; this.table = this.dom.appendChild(document.createElement('table')); this.colgroup = this.table.appendChild(document.createElement('colgroup')); updateColumns(node, this.colgroup, this.table, cellMinWidth); this.contentDOM = this.table.appendChild(document.createElement('tbody')); } update(node) { if (node.type != this.node.type) return false; this.node = node; updateColumns(node, this.colgroup, this.table, this.cellMinWidth); return true; } ignoreMutation(record) { return record.type == 'attributes' && (record.target == this.table || this.colgroup.contains(record.target)); } } function updateColumns(node, colgroup, table, cellMinWidth, overrideCol, overrideValue) { let totalWidth = 0, fixedWidth = true; let nextDOM = colgroup.firstChild, row = node.firstChild; for (let i = 0, col = 0; i < row.childCount; i++) { let { colspan, colwidth } = row.child(i).attrs; for (let j = 0; j < colspan; j++, col++) { let hasWidth = overrideCol == col ? overrideValue : colwidth && colwidth[j]; let cssWidth = hasWidth ? hasWidth + 'px' : ''; totalWidth += hasWidth || cellMinWidth; if (!hasWidth) fixedWidth = false; if (!nextDOM) { colgroup.appendChild(document.createElement('col')).style.width = cssWidth; } else { if (nextDOM.style.width != cssWidth) nextDOM.style.width = cssWidth; nextDOM = nextDOM.nextSibling; } } } while (nextDOM) { let after = nextDOM.nextSibling; nextDOM.parentNode.removeChild(nextDOM); nextDOM = after; } if (fixedWidth) { table.style.width = totalWidth + 'px'; table.style.minWidth = ''; } else { table.style.width = ''; table.style.minWidth = totalWidth + 'px'; } } const key = new prosemirrorState.PluginKey('tableColumnResizing'); function columnResizing({ handleWidth = 5, cellMinWidth = 25, View = TableView, lastColumnResizable = true } = {}) { let plugin = new prosemirrorState.Plugin({ key, state: { init(_, state) { this.spec.props.nodeViews[tableNodeTypes(state.schema).table.name] = (node, view) => new View(node, cellMinWidth, view); return new ResizeState(-1, false); }, apply(tr, prev) { return prev.apply(tr); } }, props: { attributes(state) { let pluginState = key.getState(state); return pluginState.activeHandle > -1 ? { class: 'resize-cursor' } : null; }, handleDOMEvents: { mousemove(view, event) { handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable); }, mouseleave(view) { handleMouseLeave(view); }, mousedown(view, event) { handleMouseDown(view, event, cellMinWidth); } }, decorations(state) { let pluginState = key.getState(state); if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle); }, nodeViews: {} } }); return plugin; } class ResizeState { constructor(activeHandle, dragging) { this.activeHandle = activeHandle; this.dragging = dragging; } apply(tr) { let state = this, action = tr.getMeta(key); if (action && action.setHandle != null) return new ResizeState(action.setHandle, null); if (action && action.setDragging !== undefined) return new ResizeState(state.activeHandle, action.setDragging); if (state.activeHandle > -1 && tr.docChanged) { let handle = tr.mapping.map(state.activeHandle, -1); if (!pointsAtCell(tr.doc.resolve(handle))) handle = null; state = new ResizeState(handle, state.dragging); } return state; } } function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) { let pluginState = key.getState(view.state); if (!pluginState.dragging) { let target = domCellAround(event.target), cell = -1; if (target) { let { left, right } = target.getBoundingClientRect(); if (event.clientX - left <= handleWidth) cell = edgeCell(view, event, 'left');else if (right - event.clientX <= handleWidth) cell = edgeCell(view, event, 'right'); } if (cell != pluginState.activeHandle) { if (!lastColumnResizable && cell !== -1) { let $cell = view.state.doc.resolve(cell); let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; if (col == map.width - 1) { return; } } updateHandle(view, cell); } } } function handleMouseLeave(view) { let pluginState = key.getState(view.state); if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1); } function handleMouseDown(view, event, cellMinWidth) { let pluginState = key.getState(view.state); if (pluginState.activeHandle == -1 || pluginState.dragging) return false; let cell = view.state.doc.nodeAt(pluginState.activeHandle); let width = currentColWidth(view, pluginState.activeHandle, cell.attrs); view.dispatch(view.state.tr.setMeta(key, { setDragging: { startX: event.clientX, startWidth: width } })); function finish(event) { window.removeEventListener('mouseup', finish); window.removeEventListener('mousemove', move); let pluginState = key.getState(view.state); if (pluginState.dragging) { updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth)); view.dispatch(view.state.tr.setMeta(key, { setDragging: null })); } } function move(event) { if (!event.which) return finish(event); let pluginState = key.getState(view.state); let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth); displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth); } window.addEventListener('mouseup', finish); window.addEventListener('mousemove', move); event.preventDefault(); return true; } function currentColWidth(view, cellPos, { colspan, colwidth }) { let width = colwidth && colwidth[colwidth.length - 1]; if (width) return width; let dom = view.domAtPos(cellPos); let node = dom.node.childNodes[dom.offset]; let domWidth = node.offsetWidth, parts = colspan; if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) { domWidth -= colwidth[i]; parts--; } return domWidth / parts; } function domCellAround(target) { while (target && target.nodeName != 'TD' && target.nodeName != 'TH') target = target.classList.contains('ProseMirror') ? null : target.parentNode; return target; } function edgeCell(view, event, side) { let found = view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!found) return -1; let { pos } = found; let $cell = cellAround(view.state.doc.resolve(pos)); if (!$cell) return -1; if (side == 'right') return $cell.pos; let map = TableMap.get($cell.node(-1)), start = $cell.start(-1); let index = map.map.indexOf($cell.pos - start); return index % map.width == 0 ? -1 : start + map.map[index - 1]; } function draggedWidth(dragging, event, cellMinWidth) { let offset = event.clientX - dragging.startX; return Math.max(cellMinWidth, dragging.startWidth + offset); } function updateHandle(view, value) { view.dispatch(view.state.tr.setMeta(key, { setHandle: value })); } function updateColumnWidth(view, cell, width) { let $cell = view.state.doc.resolve(cell); let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; let tr = view.state.tr; for (let row = 0; row < map.height; row++) { let mapIndex = row * map.width + col; // Rowspanning cell that has already been handled if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue; let pos = map.map[mapIndex], { attrs } = table.nodeAt(pos); let index = attrs.colspan == 1 ? 0 : col - map.colCount(pos); if (attrs.colwidth && attrs.colwidth[index] == width) continue; let colwidth = attrs.colwidth ? attrs.colwidth.slice() : zeroes(attrs.colspan); colwidth[index] = width; tr.setNodeMarkup(start + pos, null, setAttr(attrs, 'colwidth', colwidth)); } if (tr.docChanged) view.dispatch(tr); } function displayColumnWidth(view, cell, width, cellMinWidth) { let $cell = view.state.doc.resolve(cell); let table = $cell.node(-1), start = $cell.start(-1); let col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; let dom = view.domAtPos($cell.start(-1)).node; while (dom.nodeName != 'TABLE') dom = dom.parentNode; updateColumns(table, dom.firstChild, dom, cellMinWidth, col, width); } function zeroes(n) { let result = []; for (let i = 0; i < n; i++) result.push(0); return result; } function handleDecorations(state, cell) { let decorations = []; let $cell = state.doc.resolve(cell); let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan; for (let row = 0; row < map.height; row++) { let index = col + row * map.width - 1; // For positions that are have either a different cell or the end // of the table to their right, and either the top of the table or // a different cell above them, add a decoration if ((col == map.width || map.map[index] != map.map[index + 1]) && (row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) { let cellPos = map.map[index]; let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1; let dom = document.createElement('div'); dom.className = 'column-resize-handle'; decorations.push(prosemirrorView.Decoration.widget(pos, dom)); } } return prosemirrorView.DecorationSet.create(state.doc, decorations); } // This file defines a plugin that handles the drawing of cell // // Creates a [plugin](http://prosemirror.net/docs/ref/#state.Plugin) // that, when added to an editor, enables cell-selection, handles // cell-based copy/paste, and makes sure tables stay well-formed (each // row has the same width, and cells don't overlap). // // You should probably put this plugin near the end of your array of // plugins, since it handles mouse and arrow key events in tables // rather broadly, and other plugins, like the gap cursor or the // column-width dragging plugin, might want to get a turn first to // perform more specific behavior. function tableEditing({ allowTableNodeSelection = false } = {}) { return new prosemirrorState.Plugin({ key: key$1, // This piece of state is used to remember when a mouse-drag // cell-selection is happening, so that it can continue even as // transactions (which might move its anchor cell) come in. state: { init() { return null; }, apply(tr, cur) { let set = tr.getMeta(key$1); if (set != null) return set == -1 ? null : set; if (cur == null || !tr.docChanged) return cur; let { deleted, pos } = tr.mapping.mapResult(cur); return deleted ? null : pos; } }, props: { decorations: drawCellSelection, handleDOMEvents: { mousedown: handleMouseDown$1 }, createSelectionBetween(view) { if (key$1.getState(view.state) != null) return view.state.selection; }, handleTripleClick, handleKeyDown, handlePaste }, appendTransaction(_, oldState, state) { return normalizeSelection(state, fixTables(state, oldState), allowTableNodeSelection); } }); } exports.CellSelection = CellSelection; exports.TableMap = TableMap; exports.TableView = TableView; exports.__clipCells = clipCells; exports.__insertCells = insertCells; exports.__pastedCells = pastedCells; exports.addColSpan = addColSpan; exports.addColumn = addColumn; exports.addColumnAfter = addColumnAfter; exports.addColumnBefore = addColumnBefore; exports.addRow = addRow; exports.addRowAfter = addRowAfter; exports.addRowBefore = addRowBefore; exports.cellAround = cellAround; exports.colCount = colCount; exports.columnIsHeader = columnIsHeader; exports.columnResizing = columnResizing; exports.columnResizingPluginKey = key; exports.deleteColumn = deleteColumn; exports.deleteRow = deleteRow; exports.deleteTable = deleteTable; exports.findCell = findCell; exports.fixTables = fixTables; exports.fixTablesKey = fixTablesKey; exports.goToNextCell = goToNextCell; exports.handlePaste = handlePaste; exports.inSameTable = inSameTable; exports.isInTable = isInTable; exports.mergeCells = mergeCells; exports.moveCellForward = moveCellForward; exports.nextCell = nextCell; exports.pointsAtCell = pointsAtCell; exports.removeColSpan = removeColSpan; exports.removeColumn = removeColumn; exports.removeRow = removeRow; exports.rowIsHeader = rowIsHeader; exports.selectedRect = selectedRect; exports.selectionCell = selectionCell; exports.setAttr = setAttr; exports.setCellAttr = setCellAttr; exports.splitCell = splitCell; exports.splitCellWithType = splitCellWithType; exports.tableEditing = tableEditing; exports.tableEditingKey = key$1; exports.tableNodeTypes = tableNodeTypes; exports.tableNodes = tableNodes; exports.toggleHeader = toggleHeader; exports.toggleHeaderCell = toggleHeaderCell; exports.toggleHeaderColumn = toggleHeaderColumn; exports.toggleHeaderRow = toggleHeaderRow; exports.updateColumnsOnResize = updateColumns; //# sourceMappingURL=index.cjs.js.map