diff --git a/wax-prosemirror-core/src/utilities/commands/Commands.js b/wax-prosemirror-core/src/utilities/commands/Commands.js index 6bbab9026b399afbb2a3c74a2ca16173a7c6f33a..64d14487222f59565c4f1c32d85e76a0f8aa9db2 100644 --- a/wax-prosemirror-core/src/utilities/commands/Commands.js +++ b/wax-prosemirror-core/src/utilities/commands/Commands.js @@ -96,8 +96,11 @@ const createTable = (colsRows, state, dispatch) => { rows.push(state.config.schema.nodes.table_row.createAndFill(null, cells)); } - const table = state.config.schema.nodes.table.createAndFill(null, rows); - dispatch(state.tr.replaceSelectionWith(table)); + const tableBody = state.config.schema.nodes.table_body.createAndFill( + null, + rows, + ); + dispatch(state.tr.replaceSelectionWith(tableBody)); }; const createLink = (state, dispatch) => { diff --git a/wax-prosemirror-services/src/TablesService/components/TableDropDown.js b/wax-prosemirror-services/src/TablesService/components/TableDropDown.js index 903075b7a30fa949b483f9f1953b5dff3e91ccba..f5112b45f25aa1480ddb6e9327a692eafef06640 100644 --- a/wax-prosemirror-services/src/TablesService/components/TableDropDown.js +++ b/wax-prosemirror-services/src/TablesService/components/TableDropDown.js @@ -76,6 +76,8 @@ const TableDropDown = ({ item }) => { { label: 'Toggle header column', value: 'toggleHeaderColumn' }, { label: 'Toggle header row', value: 'toggleHeaderRow' }, { label: 'Toggle header cells', value: 'toggleHeaderCell' }, + { label: 'Add Caption', value: 'addCaption' }, + { label: 'Delete Caption', value: 'deleteCaption' }, ]; const { activeView } = useContext(WaxContext); diff --git a/wax-prosemirror-services/src/TablesService/table.css b/wax-prosemirror-services/src/TablesService/table.css index 1737e4ac5e6407dddce1c2eaa0be6147117d96e6..692234ff6bbbc53e65594158e2335533a209335c 100644 --- a/wax-prosemirror-services/src/TablesService/table.css +++ b/wax-prosemirror-services/src/TablesService/table.css @@ -12,6 +12,12 @@ width: 100%; } + table>caption { + background-color: #535E76; + color: white; + font-weight: bold; + } + th, td { border: 1px solid #eee; diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/index.js b/wax-prosemirror-services/src/TablesService/tableSrc/index.js index ed1ddc8e6161ab6a49237f657133f926dd9a30c3..71d900556a93b4740a67f5f8482be404d7933d20 100644 --- a/wax-prosemirror-services/src/TablesService/tableSrc/index.js +++ b/wax-prosemirror-services/src/TablesService/tableSrc/index.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - // src/index.ts import { Plugin as Plugin2 } from 'prosemirror-state'; @@ -13,6 +11,153 @@ import { } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; +// src/util.ts +import { PluginKey } from 'prosemirror-state'; + +// src/schema.ts +function getCellAttrs(dom, extraAttrs) { + if (typeof dom === 'string') { + return {}; + } + const widthAttr = dom.getAttribute('data-colwidth'); + const widths = + widthAttr && /^\d+(,\d+)*$/.test(widthAttr) + ? widthAttr.split(',').map(s => Number(s)) + : null; + const colspan = Number(dom.getAttribute('colspan') || 1); + const result = { + colspan, + rowspan: Number(dom.getAttribute('rowspan') || 1), + colwidth: widths && widths.length == colspan ? widths : null, + }; + for (const prop in extraAttrs) { + const getter = extraAttrs[prop].getFromDOM; + const value = getter && getter(dom); + if (value != null) { + result[prop] = value; + } + } + return result; +} +function setCellAttrs(node, extraAttrs) { + const 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 (const prop in extraAttrs) { + const setter = extraAttrs[prop].setDOMAttr; + if (setter) setter(node.attrs[prop], attrs); + } + return attrs; +} +function tableNodes(options) { + const extraAttrs = options.cellAttributes || {}; + const cellAttrs = { + colspan: { default: 1 }, + rowspan: { default: 1 }, + colwidth: { default: null }, + }; + for (const prop in extraAttrs) + cellAttrs[prop] = { default: extraAttrs[prop].default }; + return { + table: { + // content: 'table_caption? table_head? table_body* table_foot?', + content: 'table_caption? table_body*', + tableRole: 'table', + isolating: true, + group: options.tableGroup, + parseDOM: [{ tag: 'table' }], + toDOM() { + return ['table', 0]; + }, + }, + table_caption: { + content: 'block+', + tableRole: 'caption', + isolating: true, + parseDOM: [{ tag: 'caption' }], + toDOM() { + return ['caption', 0]; + }, + }, + table_head: { + content: 'table_row+', + tableRole: 'head', + isolating: true, + parseDOM: [{ tag: 'thead' }], + toDOM() { + return ['thead', 0]; + }, + }, + table_foot: { + content: 'table_row+', + tableRole: 'foot', + isolating: true, + parseDOM: [{ tag: 'tfoot' }], + toDOM() { + return ['tfoot', 0]; + }, + }, + table_body: { + content: 'table_row+', + tableRole: 'body', + isolating: true, + parseDOM: [{ tag: 'tbody' }], + toDOM() { + return ['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 (const name in schema.nodes) { + const type = schema.nodes[name], + role = type.spec.tableRole; + if (role) result[role] = type; + } + } + return result; +} +function isTableSection(node) { + const role = node.type.spec.tableRole; + return role === 'body' || role === 'head' || role === 'foot'; +} +function isTableSectionRole(role) { + return role === 'body' || role === 'head' || role === 'foot'; +} + // src/tablemap.ts var readFromCache; var addToCache; @@ -38,10 +183,11 @@ if (typeof WeakMap != 'undefined') { }; } var TableMap = class { - constructor(width, height, map, problems) { + constructor(width, height, map, sectionRows, problems) { this.width = width; this.height = height; this.map = map; + this.sectionRows = sectionRows; this.problems = problems; } // Find the dimensions of the cell at the given position. @@ -131,20 +277,80 @@ var TableMap = class { } return result; } + // Return the indices of all sections that are touched (overlapped, even partially) + // by the given rectangle. + // Indices start from 0 and don't consider the caption, so if there's a caption + // section n is table.child(n+1), otherwise it's table.child(n) + sectionsInRect(rect) { + const result = []; + const sectionRows = this.sectionRows; + let top = 0, + bottom = 0; + for (let i = 0; i < sectionRows.length; i++) { + bottom += sectionRows[i]; + if (rect.top < bottom && rect.bottom > top) result.push(i); + top = bottom; + } + return result; + } + isLastRowInSection(row) { + const srows = this.sectionRows; + let lastRow = 0; + for (let s = 0; s < srows.length; s++) { + lastRow += srows[s]; + if (lastRow === row) return true; + if (lastRow > row) return false; + } + return false; + } // 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++) { - const rowEnd = rowStart + table.child(i).nodeSize; + for (let i = 0; ; i++) { + const { node, pos: rowStart } = getRow(table, row); + const rowEnd = rowStart + node.nodeSize; if (i == row) { let index = col + row * this.width; const rowEndIndex = (row + 1) * this.width; while (index < rowEndIndex && this.map[index] < rowStart) index++; return index == rowEndIndex ? rowEnd - 1 : this.map[index]; } - rowStart = rowEnd; } } + findSection(pos) { + const { top } = this.findCell(pos); + let rows = 0, + nextRows = 0; + for (let s = 0; s < this.sectionRows.length; s++) { + const nextRows2 = rows + this.sectionRows[s]; + if (top < rows) + return { + left: 0, + top: rows, + right: this.width, + bottom: nextRows2, + }; + rows = nextRows2; + } + return { + left: 0, + top: 0, + right: this.width, + bottom: this.height, + }; + } + sectionOfRow(row) { + let countRows = 0; + for (let i = 0; i < this.sectionRows.length; i++) { + countRows += this.sectionRows[i]; + if (row < countRows) return i; + } + return -1; + } + rectOverOneSection(rect) { + const topSection = this.sectionOfRow(rect.top); + return topSection >= 0 && topSection == this.sectionOfRow(rect.bottom - 1); + } // Find the table map for the given table node. static get(table) { return readFromCache(table) || addToCache(table, computeMap(table)); @@ -153,15 +359,39 @@ var TableMap = class { function computeMap(table) { if (table.type.spec.tableRole != 'table') throw new RangeError('Not a table node: ' + table.type.name); - const width = findWidth(table), - height = table.childCount; + const width = findWidth(table); + const height = findHeight(table); + const tmap = new TableMap(width, height, [], [], null); + let offset = 0; + let colWidths = []; + for (let c = 0; c < table.childCount; c++) { + const section = table.child(c); + if (isTableSection(section)) { + tmap.sectionRows.push(section.childCount); + let smap = computeSectionMap(section, width, offset + 1, colWidths); + tmap.map = tmap.map.concat(smap.map); + if (smap.problems) { + tmap.problems = (tmap.problems || []).concat(smap.problems); + } + } + offset += section.nodeSize; + } + let badWidths = false; + for (let i = 0; !badWidths && i < colWidths.length; i += 2) + if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true; + if (badWidths) findBadColWidths(tmap, colWidths, table); + return tmap; +} +function computeSectionMap(section, width, offset, colWidths) { + if (!isTableSection(section)) + throw new RangeError('Not a table section node: ' + section.type.name); + const height = section.childCount; const map = []; let mapPos = 0; let problems = null; - const colWidths = []; for (let i = 0, e = width * height; i < e; i++) map[i] = 0; - for (let row = 0, pos = 0; row < height; row++) { - const rowNode = table.child(row); + for (let row = 0, pos = offset; row < height; row++) { + const rowNode = section.child(row); pos++; for (let i = 0; ; i++) { while (mapPos < map.length && map[mapPos] != 0) mapPos++; @@ -213,37 +443,48 @@ function computeMap(table) { (problems || (problems = [])).push({ type: 'missing', row, n: missing }); pos++; } - const tableMap = new TableMap(width, height, map, problems); - let badWidths = false; - 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); + const tableMap = new TableMap(width, height, map, [], problems); return tableMap; } function findWidth(table) { let width = -1; let hasRowSpan = false; - for (let row = 0; row < table.childCount; row++) { - const rowNode = table.child(row); - let rowWidth = 0; - if (hasRowSpan) - for (let j = 0; j < row; j++) { - const prevRow = table.child(j); - for (let i = 0; i < prevRow.childCount; i++) { - const cell = prevRow.child(i); - if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan; + for (let cIndex = 0; cIndex < table.childCount; cIndex++) { + const sectionNode = table.child(cIndex); + if (isTableSection(sectionNode)) { + for (let row = 0; row < sectionNode.childCount; row++) { + const rowNode = sectionNode.child(row); + let rowWidth = 0; + if (hasRowSpan) + for (let j = 0; j < row; j++) { + const prevRow = sectionNode.child(j); + for (let i = 0; i < prevRow.childCount; i++) { + const cell = prevRow.child(i); + if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan; + } + } + for (let i = 0; i < rowNode.childCount; i++) { + const 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); } - for (let i = 0; i < rowNode.childCount; i++) { - const 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 findHeight(table) { + let height = 0; + for (let cIndex = 0; cIndex < table.childCount; cIndex++) { + const sectionNode = table.child(cIndex); + if (isTableSection(sectionNode)) { + height += sectionNode.childCount; + } + } + return height; +} function findBadColWidths(map, colWidths, table) { if (!map.problems) map.problems = []; const seen = {}; @@ -266,12 +507,13 @@ function findBadColWidths(map, colWidths, table) { ) (updated || (updated = freshColWidth(attrs)))[j] = colWidth; } - if (updated) + if (updated) { map.problems.unshift({ type: 'colwidth mismatch', pos, colwidth: updated, }); + } } } function freshColWidth(attrs) { @@ -281,127 +523,6 @@ function freshColWidth(attrs) { return result; } -// src/util.ts -import { PluginKey } from 'prosemirror-state'; - -// src/schema.ts -function getCellAttrs(dom, extraAttrs) { - if (typeof dom === 'string') { - return {}; - } - const widthAttr = dom.getAttribute('data-colwidth'); - const widths = - widthAttr && /^\d+(,\d+)*$/.test(widthAttr) - ? widthAttr.split(',').map(s => Number(s)) - : null; - const colspan = Number(dom.getAttribute('colspan') || 1); - const result = { - colspan, - rowspan: Number(dom.getAttribute('rowspan') || 1), - colwidth: widths && widths.length == colspan ? widths : null, - }; - for (const prop in extraAttrs) { - const getter = extraAttrs[prop].getFromDOM; - const value = getter && getter(dom); - if (value != null) { - result[prop] = value; - } - } - return result; -} -function setCellAttrs(node, extraAttrs) { - const 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 (const prop in extraAttrs) { - const setter = extraAttrs[prop].setDOMAttr; - if (setter) setter(node.attrs[prop], attrs); - } - return attrs; -} -function tableNodes(options) { - const extraAttrs = options.cellAttributes || {}; - const cellAttrs = { - colspan: { default: 1 }, - rowspan: { default: 1 }, - colwidth: { default: null }, - }; - for (const prop in extraAttrs) - cellAttrs[prop] = { default: extraAttrs[prop].default }; - return { - table: { - content: 'table_caption? table_body*', - tableRole: 'table', - isolating: true, - group: options.tableGroup, - parseDOM: [{ tag: 'table' }], - toDOM() { - return ['table', 0]; - }, - }, - table_caption: { - content: 'block+', - tableRole: 'caption', - isolating: true, - parseDOM: [{ tag: 'caption' }], - toDOM() { - return ['caption', 0]; - }, - }, - table_body: { - content: 'table_row+', - tableRole: 'body', - isolating: true, - parseDOM: [{ tag: 'tbody' }], - toDOM() { - return ['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 (const name in schema.nodes) { - const type = schema.nodes[name], - role = type.spec.tableRole; - if (role) result[role] = type; - } - } - return result; -} - // src/util.ts var tableEditingKey = new PluginKey('selectingCells'); function cellAround($pos) { @@ -423,6 +544,11 @@ function isInTable(state) { if ($head.node(d).type.spec.tableRole == 'row') return true; return false; } +function tableDepth($pos) { + for (let d = $pos.depth; d >= 0; d--) + if ($pos.node(d).type.spec.tableRole == 'table') return d; + return -1; +} function selectionCell(state) { const sel = state.selection; if ('$anchorCell' in sel && sel.$anchorCell) { @@ -470,20 +596,20 @@ function moveCellForward($pos) { function inSameTable($cellA, $cellB) { return ( $cellA.depth == $cellB.depth && - $cellA.pos >= $cellB.start(-1) && - $cellA.pos <= $cellB.end(-1) + $cellA.pos >= $cellB.start(-2) && + $cellA.pos <= $cellB.end(-2) ); } function findCell($pos) { - return TableMap.get($pos.node(-1)).findCell($pos.pos - $pos.start(-1)); + return TableMap.get($pos.node(-2)).findCell($pos.pos - $pos.start(-2)); } function colCount($pos) { - return TableMap.get($pos.node(-1)).colCount($pos.pos - $pos.start(-1)); + return TableMap.get($pos.node(-2)).colCount($pos.pos - $pos.start(-2)); } function nextCell($pos, axis, dir) { - const table = $pos.node(-1); + const table = $pos.node(-2); const map = TableMap.get(table); - const tableStart = $pos.start(-1); + const tableStart = $pos.start(-2); const moved = map.nextCell($pos.pos - tableStart, axis, dir); return moved == null ? null : $pos.node(0).resolve(tableStart + moved); } @@ -494,22 +620,145 @@ function removeColSpan(attrs, pos, n = 1) { result.colwidth.splice(pos, n); if (!result.colwidth.some(w => w > 0)) result.colwidth = null; } - return result; + return result; +} +function addColSpan(attrs, pos, n = 1) { + const result = { ...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) { + const 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; +} +function rowsCount(table) { + let count = 0; + for (let c = 0; c < table.childCount; c++) { + const section = table.child(c); + if (isTableSection(section)) count += section.childCount; + } + return count; +} +function getRow(table, row) { + let rPos = 0; + let prevSectionsRows = 0; + let sectionIndex = -1; + for (let tc = 0; tc < table.childCount; tc++) { + const section = table.child(tc); + if (isTableSection(section)) { + sectionIndex++; + const sectionRows = section.childCount; + if (sectionRows > 0) { + if (prevSectionsRows + sectionRows <= row) { + if (tc === table.childCount - 1) { + return { + node: null, + pos: rPos + section.nodeSize - 1, + section: sectionIndex, + }; + } + rPos += section.nodeSize; + prevSectionsRows += sectionRows; + } else { + rPos++; + let r = 0; + while (r < sectionRows) { + if (prevSectionsRows + r === row) break; + rPos += section.child(r).nodeSize; + r++; + } + if (r === sectionRows) rPos++; + return { + node: r >= sectionRows ? null : section.child(r), + pos: rPos, + section: sectionIndex, + }; + } + } + } else { + rPos += section.nodeSize; + } + } + return { node: null, pos: rPos, section: sectionIndex }; +} +function rowPos(table, row) { + return getRow( + table, + row, + /* debug */ + ).pos; +} +function rowAtPos(table, pos) { + let rpos = 0; + let row = 0; + for (let c = 0; c < table.childCount; c++) { + const section = table.child(c); + if (isTableSection(section)) { + rpos++; + for (let r = 0; r < section.childCount; r++) { + rpos += section.child(r).nodeSize; + if (pos < rpos) return row; + row++; + } + rpos++; + } else { + rpos += section.nodeSize; + } + } + return row; +} +function tableHasCaption(table) { + if (table && table.type.spec.tableRole === 'table') { + return table.child(0).type.spec.tableRole === 'caption'; + } + return false; +} +function tableHasHead(table) { + if (table && table.type.spec.tableRole === 'table') { + for (let i = 0; i < table.childCount; i++) + if (table.child(i).type.spec.tableRole === 'head') return true; + } + return false; +} +function tableHasFoot(table) { + if (table && table.type.spec.tableRole === 'table') { + for (let i = table.childCount - 1; i > 0; i--) + if (table.child(i).type.spec.tableRole === 'foot') return true; + } + return false; } -function addColSpan(attrs, pos, n = 1) { - const result = { ...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); +function tableBodiesCount(table) { + let count = 0; + if (table && table.type.spec.tableRole === 'table') { + for (let i = 0; i < table.childCount; i++) + if (table.child(i).type.spec.tableRole === 'body') count++; } - return result; + return count; } -function columnIsHeader(map, table, col) { - const 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; +function tableSectionsCount(table) { + let count = 0; + if (table && table.type.spec.tableRole === 'table') { + for (let i = 0; i < table.childCount; i++) + if (isTableSection(table.child(i))) count++; + } + return count; +} +function isRowLastInSection(table, row) { + const { height, sectionRows } = TableMap.get(table); + if (row >= height || row < 0) return false; + let rowsMinusOne = -1; + for (let i = 0; i < sectionRows.length; i++) { + rowsMinusOne += sectionRows[i]; + if (row === rowsMinusOne) return true; + if (row < rowsMinusOne) return false; + } + return false; } // src/cellselection.ts @@ -519,9 +768,9 @@ var CellSelection = class extends Selection { // cells in the same table. They may be the same, to select a single // cell. constructor($anchorCell, $headCell = $anchorCell) { - const table = $anchorCell.node(-1); + const table = $anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); + const tableStart = $anchorCell.start(-2); const rect = map.rectBetween( $anchorCell.pos - tableStart, $headCell.pos - tableStart, @@ -554,7 +803,7 @@ var CellSelection = class extends Selection { pointsAtCell($headCell) && inSameTable($anchorCell, $headCell) ) { - const tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1); + const tableChanged = this.$anchorCell.node(-2) != $anchorCell.node(-2); if (tableChanged && this.isRowSelection()) return CellSelection.rowSelection($anchorCell, $headCell); else if (tableChanged && this.isColSelection()) @@ -566,9 +815,9 @@ var CellSelection = class extends Selection { // Returns a rectangular slice of table rows containing the selected // cells. content() { - const table = this.$anchorCell.node(-1); + const table = this.$anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); + const tableStart = this.$anchorCell.start(-2); const rect = map.rectBetween( this.$anchorCell.pos - tableStart, this.$headCell.pos - tableStart, @@ -630,10 +879,11 @@ var CellSelection = class extends Selection { } rowContent.push(cell); } - rows.push(table.child(row).copy(Fragment.from(rowContent))); + const rowNode = getRow(table, row).node; + rows.push(rowNode.copy(Fragment.from(rowContent))); } const fragment = - this.isColSelection() && this.isRowSelection() ? table : rows; + this.isColSelection(map) && this.isRowSelection() ? table : rows; return new Slice(Fragment.from(fragment), 1, 1); } replace(tr, content = Slice.empty) { @@ -658,9 +908,9 @@ var CellSelection = class extends Selection { this.replace(tr, new Slice(Fragment.from(node), 0, 0)); } forEachCell(f) { - const table = this.$anchorCell.node(-1); + const table = this.$anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); + const tableStart = this.$anchorCell.start(-2); const cells = map.cellsInRect( map.rectBetween( this.$anchorCell.pos - tableStart, @@ -673,22 +923,22 @@ var CellSelection = class extends Selection { } // True if this selection goes all the way from the top to the // bottom of the table. - isColSelection() { - const anchorTop = this.$anchorCell.index(-1); - const headTop = this.$headCell.index(-1); + isColSelection(tableMap) { + const table = this.$anchorCell.node(-2); + const tableStart = this.$anchorCell.start(-2); + const anchorTop = rowAtPos(table, this.$anchorCell.pos - tableStart); + const headTop = rowAtPos(table, this.$headCell.pos - tableStart); if (Math.min(anchorTop, headTop) > 0) return false; const anchorBottom = anchorTop + this.$anchorCell.nodeAfter.attrs.rowspan; const headBottom = headTop + this.$headCell.nodeAfter.attrs.rowspan; - return ( - Math.max(anchorBottom, headBottom) == this.$headCell.node(-1).childCount - ); + return Math.max(anchorBottom, headBottom) == rowsCount(table); } // Returns the smallest column selection that covers the given anchor // and head cell. static colSelection($anchorCell, $headCell = $anchorCell) { - const table = $anchorCell.node(-1); + const table = $anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); + const tableStart = $anchorCell.start(-2); const anchorRect = map.findCell($anchorCell.pos - tableStart); const headRect = map.findCell($headCell.pos - tableStart); const doc = $anchorCell.node(0); @@ -714,9 +964,9 @@ var CellSelection = class extends Selection { // True if this selection goes all the way from the left to the // right of the table. isRowSelection() { - const table = this.$anchorCell.node(-1); + const table = this.$anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); + const tableStart = this.$anchorCell.start(-2); const anchorLeft = map.colCount(this.$anchorCell.pos - tableStart); const headLeft = map.colCount(this.$headCell.pos - tableStart); if (Math.min(anchorLeft, headLeft) > 0) return false; @@ -734,9 +984,9 @@ var CellSelection = class extends Selection { // Returns the smallest row selection that covers the given anchor // and head cell. static rowSelection($anchorCell, $headCell = $anchorCell) { - const table = $anchorCell.node(-1); + const table = $anchorCell.node(-2); const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); + const tableStart = $anchorCell.start(-2); const anchorRect = map.findCell($anchorCell.pos - tableStart); const headRect = map.findCell($headCell.pos - tableStart); const doc = $anchorCell.node(0); @@ -759,6 +1009,33 @@ var CellSelection = class extends Selection { } return new CellSelection($anchorCell, $headCell); } + // Returns the smallest section selection that covers the given anchor + // and head cell. + static sectionSelection($anchorCell, $headCell = $anchorCell) { + const table = $anchorCell.node(-2); + const map = TableMap.get(table); + const tableStart = $anchorCell.start(-2); + const sectionStart = $anchorCell.start(-1); + const anchorSection = map.findSection($anchorCell.pos - sectionStart + 1); + const headSection = map.findSection($headCell.pos - sectionStart + 1); + const doc = $anchorCell.node(0); + if (anchorSection.top <= headSection.top) { + $anchorCell = doc.resolve( + tableStart + map.map[map.width * anchorSection.top], + ); + $headCell = doc.resolve( + tableStart + map.map[map.width * headSection.bottom - 1], + ); + } else { + $anchorCell = doc.resolve( + tableStart + map.map[map.width * headSection.top], + ); + $headCell = doc.resolve( + tableStart + map.map[map.width * anchorSection.bottom - 1], + ); + } + return new CellSelection($anchorCell, $headCell); + } toJSON() { return { type: 'cell', @@ -821,7 +1098,7 @@ function isCellBoundarySelection({ $from, $to }) { if ($to.before(d + 1) > $to.start(d)) break; return ( afterFrom == beforeTo && - /row|table/.test($from.node(depth).type.spec.tableRole) + /^(row|body|table|head|foot)$/.test($from.node(depth).type.spec.tableRole) ); } function isTextSelectionAcrossCells({ $from, $to }) { @@ -860,11 +1137,14 @@ function normalizeSelection(state, tr, allowTableNodeSelection) { } else if (role == 'row') { const $cell = doc.resolve(sel.from + 1); normalize = CellSelection.rowSelection($cell, $cell); + } else if (isTableSectionRole(role)) { + const $cell = doc.resolve(sel.from + 2); + normalize = CellSelection.sectionSelection($cell, $cell); } else if (!allowTableNodeSelection) { const map = TableMap.get(sel.node); const start = sel.from + 1; const lastCell = start + map.map[map.width * map.height - 1]; - normalize = CellSelection.create(doc, start + 1, lastCell); + normalize = CellSelection.create(doc, start + 2, lastCell); } } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) { normalize = TextSelection.create(doc, sel.from); @@ -909,6 +1189,7 @@ function fixTables(state, oldState) { return tr; } function fixTable(state, table, tablePos, tr) { + if (hasFreeRows(table)) tr = fixFreeRows(state, table, tablePos, tr); const map = TableMap.get(table); if (!map.problems) return tr; if (!tr) tr = state.tr; @@ -950,8 +1231,8 @@ function fixTable(state, table, tablePos, tr) { if (first == null) first = i; last = i; } - for (let i = 0, pos = tablePos + 1; i < map.height; i++) { - const row = table.child(i); + for (let i = 0; i < map.height; i++) { + const { node: row, pos } = getRow(table, i); const end = pos + row.nodeSize; const add = mustAdd[i]; if (add > 0) { @@ -965,12 +1246,45 @@ function fixTable(state, table, tablePos, tr) { if (node) nodes.push(node); } const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1; - tr.insert(tr.mapping.map(side), nodes); + tr.insert(tr.mapping.map(side + 1), nodes); } - pos = end; } return tr.setMeta(fixTablesKey, { fixTables: true }); } +function hasFreeRows(table) { + for (let i = 0; i < table.childCount; i++) + if (table.child(i).type.spec.tableRole === 'row') return true; + return false; +} +function fixFreeRows(state, table, tablePos, tr) { + let freeRows = []; + let freeRowsFound = false; + const sections = []; + const types = tableNodeTypes(state.schema); + for (let i = 0; i < table.childCount; i++) { + const child = table.child(i); + if (child.type.spec.tableRole === 'row') { + freeRowsFound = true; + freeRows.push(child); + } else { + if (freeRows.length > 0) { + sections.push(types.body.createAndFill(null, freeRows)); + freeRows = []; + } + sections.push(child); + } + } + if (freeRows.length > 0) { + sections.push(types.body.createAndFill(null, freeRows)); + freeRows = []; + } + if (!freeRowsFound) return tr; + return (tr || state.tr).replaceWith( + tablePos, + tablePos + table.nodeSize, + types.table.createAndFill(table.attrs, sections), + ); +} // src/input.ts import { Fragment as Fragment3, Slice as Slice3 } from 'prosemirror-model'; @@ -997,9 +1311,27 @@ function pastedCells(slice) { } const first = content.child(0); const role = first.type.spec.tableRole; - const schema = first.type.schema, - rows = []; - if (role == 'row') { + const schema = first.type.schema; + const rows = []; + if (isTableSectionRole(role)) { + for (let s = 0; s < content.childCount; s++) { + const section = content.child(s); + if (isTableSection(section)) { + for (let i = 0; i < section.childCount; i++) { + let cells = section.child(i).content; + const left = i != 0 ? 0 : Math.max(0, openStart - 1); + const right = + i < section.childCount - 1 ? 0 : Math.max(0, openEnd - 1); + if (left || right) + cells = fitSlice( + tableNodeTypes(schema).row, + new Slice2(cells, left, right), + ).content; + rows.push(cells); + } + } + } + } else if (role == 'row') { for (let i = 0; i < content.childCount; i++) { let cells = content.child(i).content; const left = i ? 0 : Math.max(0, openStart - 1); @@ -1113,16 +1445,23 @@ function growTable(tr, map, table, start, width, height, mapFrom) { let empty; let emptyHead; if (width > map.width) { - for (let row = 0, rowEnd = 0; row < map.height; row++) { - const rowNode = table.child(row); - rowEnd += rowNode.nodeSize; + const lastCellOfRow = []; + for (let row = 0; row < map.height; row++) { + const lastCell = table.nodeAt(map.map[(row + 1) * map.width - 1]); + if (lastCell == null || lastCell.type == types.cell) { + lastCellOfRow.push(empty || (empty = types.cell.createAndFill())); + } else { + lastCellOfRow.push( + emptyHead || (emptyHead = types.header_cell.createAndFill()), + ); + } + } + for (let row = 0; row < map.height; row++) { + const { node: rowNode, pos: rowPos2 } = getRow(table, row); + const rowEnd = rowPos2 + rowNode.nodeSize - 1; const cells = []; - let 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); + for (let i = map.width; i < width; i++) cells.push(lastCellOfRow[row]); + tr.insert(tr.mapping.slice(mapFrom).map(rowEnd + start), cells); } } if (height > map.height) { @@ -1145,7 +1484,7 @@ function growTable(tr, map, table, start, width, height, mapFrom) { const emptyRow = types.row.create(null, Fragment2.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); + tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 3), rows); } return !!(empty || emptyHead); } @@ -1259,6 +1598,8 @@ var handleKeyDown = keydownHandler({ ArrowRight: arrow('horiz', 1), ArrowUp: arrow('vert', -1), ArrowDown: arrow('vert', 1), + Tab: tabulation(1), + 'Shift-Tab': tabulation(-1), 'Shift-ArrowLeft': shiftArrow('horiz', -1), 'Shift-ArrowRight': shiftArrow('horiz', 1), 'Shift-ArrowUp': shiftArrow('vert', -1), @@ -1273,6 +1614,63 @@ function maybeSetSelection(state, dispatch, selection) { if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView()); return true; } +function tabulation(dir) { + return (state, dispatch, view) => { + if (!view) return false; + const sel = state.selection; + const r = state.doc.resolve(sel.head); + let d = r.depth; + let inCaption = false; + for (; d > 0; d--) { + const role = r.node(d).type.spec.tableRole; + if (role === 'row') break; + if (role === 'caption' && dir > 0) { + inCaption = true; + break; + } + } + const tableDepth2 = d - (inCaption ? 1 : 2); + const table = r.node(tableDepth2); + if (!table || table.type.spec.tableRole != 'table') return false; + const tableStart = r.start(tableDepth2); + const tmap = TableMap.get(table); + let nextCellPos; + if (inCaption) { + nextCellPos = tmap.map[0]; + } else { + const map = tmap.map; + const cellStart = inCaption + ? tmap.positionAt(0, 0, table) + : r.start(d + 1); + const cellPos = cellStart - tableStart - 1; + let i; + for ( + i = dir < 0 ? 0 : map.length - 1; + i >= 0 && i < map.length; + i -= dir + ) { + if (cellPos == map[i]) break; + } + if (i < 0 || i >= map.length) return false; + i += dir; + if (i < 0 || i >= map.length) return false; + nextCellPos = map[i]; + } + if (nextCellPos) { + const cell = table.nodeAt(nextCellPos); + if (!cell) return false; + if (dispatch) { + const from = tableStart + nextCellPos; + const to = from + cell.nodeSize - 1; + dispatch( + state.tr.setSelection(TextSelection2.create(state.doc, from, to)), + ); + } + return true; + } + return false; + }; +} function arrow(axis, dir) { return (state, dispatch, view) => { if (!view) return false; @@ -1285,8 +1683,7 @@ function arrow(axis, dir) { ); } if (axis != 'horiz' && !sel.empty) return false; - const end = atEndOfCell(view, axis, dir); - if (end == null) return false; + const end = atEndOfCell(view, axis, dir, true); if (axis == 'horiz') { return maybeSetSelection( state, @@ -1294,14 +1691,50 @@ function arrow(axis, dir) { Selection2.near(state.doc.resolve(sel.head + dir), dir), ); } else { - const $cell = state.doc.resolve(end); - const $next = nextCell($cell, axis, dir); let newSel; - if ($next) newSel = Selection2.near($next, 1); - else if (dir < 0) - newSel = Selection2.near(state.doc.resolve($cell.before(-1)), -1); - else newSel = Selection2.near(state.doc.resolve($cell.after(-1)), 1); - return maybeSetSelection(state, dispatch, newSel); + if (end) { + const $cell = state.doc.resolve(end); + if ($cell.node().type.spec.tableRole === 'row') { + const $next = nextCell($cell, axis, dir); + if ($next) newSel = Selection2.near($next, 1); + else if (dir < 0) { + const table = $cell.node(-2); + if (tableHasCaption(table)) + newSel = Selection2.near(state.doc.resolve($cell.start(-2)), 1); + else + newSel = Selection2.near(state.doc.resolve($cell.before(-2)), -1); + } else + newSel = Selection2.near(state.doc.resolve($cell.after(-2)), 1); + } else { + if (dir < 0) { + newSel = Selection2.near(state.doc.resolve($cell.before()), -1); + } else { + const table = $cell.node(); + const map = TableMap.get(table); + const pos = $cell.start() + map.positionAt(0, 0, table); + newSel = Selection2.near(state.doc.resolve(pos), 1); + } + } + } else { + if (dir > 0) { + const pos = sel.$anchor.after(); + const table = state.doc.nodeAt(pos); + if (table && table.type.spec.tableRole === 'table') + newSel = Selection2.near(state.doc.resolve(pos), 1); + } else { + newSel = Selection2.near(state.doc.resolve(sel.$anchor.before()), -1); + const d = tableDepth(newSel.$anchor); + if (d >= 0) { + const table = newSel.$anchor.node(d); + const map = TableMap.get(table); + const pos = + newSel.$anchor.start(d) + + map.positionAt(map.height - 1, 0, table); + newSel = Selection2.near(state.doc.resolve(pos), 1); + } + } + } + return newSel ? maybeSetSelection(state, dispatch, newSel) : false; } }; } @@ -1367,8 +1800,8 @@ function handlePaste(view, _, slice) { ), ], }; - const table = sel.$anchorCell.node(-1); - const start = sel.$anchorCell.start(-1); + const table = sel.$anchorCell.node(-2); + const start = sel.$anchorCell.start(-2); const rect = TableMap.get(table).rectBetween( sel.$anchorCell.pos - start, sel.$headCell.pos - start, @@ -1378,12 +1811,12 @@ function handlePaste(view, _, slice) { return true; } else if (cells) { const $cell = selectionCell(view.state); - const start = $cell.start(-1); + const start = $cell.start(-2); insertCells( view.state, view.dispatch, start, - TableMap.get($cell.node(-1)).findCell($cell.pos - start), + TableMap.get($cell.node(-2)).findCell($cell.pos - start), cells, ); return true; @@ -1448,16 +1881,19 @@ function handleMouseDown(view, startEvent) { view.root.addEventListener('dragstart', stop); view.root.addEventListener('mousemove', move); } -function atEndOfCell(view, axis, dir) { +function atEndOfCell(view, axis, dir, checkCaption = false) { if (!(view.state.selection instanceof TextSelection2)) return null; const { $head } = view.state.selection; for (let d = $head.depth - 1; d >= 0; d--) { const parent = $head.node(d), index = dir < 0 ? $head.index(d) : $head.indexAfter(d); if (index != (dir < 0 ? 0 : parent.childCount)) return null; + const alsoInCaption = + checkCaption && parent.type.spec.tableRole == 'caption'; if ( parent.type.spec.tableRole == 'cell' || - parent.type.spec.tableRole == 'header_cell' + parent.type.spec.tableRole == 'header_cell' || + alsoInCaption ) { const cellPos = $head.before(d); const dirStr = @@ -1490,95 +1926,34 @@ import { Decoration as Decoration2, DecorationSet as DecorationSet2, } from 'prosemirror-view'; - -// src/tableview.ts -var TableView = class { - 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')); - updateColumnsOnResize(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; - updateColumnsOnResize(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 updateColumnsOnResize( - node, - colgroup, - table, - cellMinWidth, - overrideCol, - overrideValue, -) { - var _a; - let totalWidth = 0; - let fixedWidth = true; - let nextDOM = colgroup.firstChild; - const row = node.firstChild; - if (!row) return; - for (let i = 0, col = 0; i < row.childCount; i++) { - const { colspan, colwidth } = row.child(i).attrs; - for (let j = 0; j < colspan; j++, col++) { - const hasWidth = - overrideCol == col ? overrideValue : colwidth && colwidth[j]; - const 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) { - const after = nextDOM.nextSibling; - (_a = nextDOM.parentNode) == null ? void 0 : _a.removeChild(nextDOM); - nextDOM = after; - } - if (fixedWidth) { - table.style.width = totalWidth + 'px'; - table.style.minWidth = ''; - } else { - table.style.width = ''; - table.style.minWidth = totalWidth + 'px'; - } -} - -// src/columnresizing.ts var columnResizingPluginKey = new PluginKey3('tableColumnResizing'); +var SPEC_COL_WIDTHS = 'colgroup'; +var SPEC_TABLE_WIDTH = 'tablewidth'; +var DEFAULT_HANDLE_WIDTH = 5; +var DEFAULT_CELL_MIN_WIDTH = 25; +var DEFAULT_LAST_COLUMN_RESIZABLE = true; function columnResizing({ - handleWidth = 5, - cellMinWidth = 25, - View = TableView, - lastColumnResizable = true, + handleWidth = DEFAULT_HANDLE_WIDTH, + cellMinWidth = DEFAULT_CELL_MIN_WIDTH, + lastColumnResizable = DEFAULT_LAST_COLUMN_RESIZABLE, } = {}) { const plugin = new Plugin({ + options: { + handleWidth, + cellMinWidth, + lastColumnResizable, + }, key: columnResizingPluginKey, state: { init(_, state) { - plugin.spec.props.nodeViews[tableNodeTypes(state.schema).table.name] = ( - node, - view, - ) => new View(node, cellMinWidth, view); - return new ResizeState(-1, false); + return new ResizeState( + -1, + false, + DecorationSet2.create( + state.doc, + createTableDecorations(state.doc, cellMinWidth), + ), + ); }, apply(tr, prev) { return prev.apply(tr); @@ -1610,33 +1985,96 @@ function columnResizing({ }, decorations: state => { const pluginState = columnResizingPluginKey.getState(state); - if (pluginState && pluginState.activeHandle > -1) { - return handleDecorations(state, pluginState.activeHandle); + let decos = DecorationSet2.empty; + if (pluginState) { + decos = decos.add( + state.doc, + pluginState.tableDecos.find(void 0, void 0, () => true), + ); + if (pluginState.activeHandle > -1) { + decos = decos.add( + state.doc, + handleDecorations(state, pluginState.activeHandle), + ); + } } + return decos; }, - nodeViews: {}, + // nodeViews: {}, }, }); return plugin; } var ResizeState = class { - constructor(activeHandle, dragging) { + constructor(activeHandle, dragging, tableDecos) { this.activeHandle = activeHandle; this.dragging = dragging; + this.tableDecos = tableDecos; } apply(tr) { const state = this; + if (tr.docChanged) { + state.tableDecos = state.tableDecos.map(tr.mapping, tr.doc); + } const action = tr.getMeta(columnResizingPluginKey); - if (action && action.setHandle != null) - return new ResizeState(action.setHandle, false); - if (action && action.setDragging !== void 0) - return new ResizeState(state.activeHandle, action.setDragging); - if (state.activeHandle > -1 && tr.docChanged) { + if (action) { + if (action.setHandle != null) + return new ResizeState(action.setHandle, false, state.tableDecos); + if (action.setDragging !== void 0) + return new ResizeState( + state.activeHandle, + action.setDragging, + state.tableDecos, + ); + let decos = state.tableDecos; + if (action.setColWidths) { + const scws = action.setColWidths; + scws.forEach(scw => { + const removed = decos.find( + scw.tableStart - 1, + scw.tableStart, + spec => spec.type === SPEC_COL_WIDTHS, + ); + if (removed) decos = decos.remove(removed); + const deco = colgroupDecoration(scw.tableStart, scw.colWidths); + decos = decos.add(tr.doc, [deco]); + }); + } + if (action.setTableWidth) { + let decos2 = state.tableDecos; + const stws = action.setTableWidth; + stws.forEach(stw => { + const removed = decos2.find( + stw.pos, + stw.pos + 1, + spec => spec.type === SPEC_TABLE_WIDTH, + ); + if (removed) { + const newDecos = []; + removed.forEach(r => { + const pos = tr.mapping.map(stw.pos); + const table = tr.doc.nodeAt(pos); + if ( + (table == null ? void 0 : table.type.spec.tableRole) === 'table' + ) { + newDecos.push( + tableWidthDecoration(pos, pos + table.nodeSize, stw.css), + ); + } + }); + if (newDecos) decos2 = decos2.remove(removed).add(tr.doc, newDecos); + } + }); + } + if (decos !== state.tableDecos) + return new ResizeState(state.activeHandle, state.dragging, decos); + } + if (tr.docChanged && state.activeHandle > -1) { let handle = tr.mapping.map(state.activeHandle, -1); if (!pointsAtCell(tr.doc.resolve(handle))) { handle = -1; } - return new ResizeState(handle, state.dragging); + return new ResizeState(handle, state.dragging, state.tableDecos); } return state; } @@ -1656,16 +2094,16 @@ function handleMouseMove( if (target) { const { left, right } = target.getBoundingClientRect(); if (event.clientX - left <= handleWidth) - cell = edgeCell(view, event, 'left', handleWidth); + cell = edgeCell(view, event, 'left'); else if (right - event.clientX <= handleWidth) - cell = edgeCell(view, event, 'right', handleWidth); + cell = edgeCell(view, event, 'right'); } if (cell != pluginState.activeHandle) { if (!lastColumnResizable && cell !== -1) { const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1); + const table = $cell.node(-2); const map = TableMap.get(table); - const tableStart = $cell.start(-1); + const tableStart = $cell.start(-2); const col = map.colCount($cell.pos - tableStart) + $cell.nodeAfter.attrs.colspan - @@ -1752,19 +2190,15 @@ function domCellAround(target) { : target.parentNode; return target; } -function edgeCell(view, event, side, handleWidth) { - const offset = side == 'right' ? -handleWidth : handleWidth; - const found = view.posAtCoords({ - left: event.clientX + offset, - top: event.clientY, - }); +function edgeCell(view, event, side) { + const found = view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!found) return -1; const { pos } = found; const $cell = cellAround(view.state.doc.resolve(pos)); if (!$cell) return -1; if (side == 'right') return $cell.pos; - const map = TableMap.get($cell.node(-1)), - start = $cell.start(-1); + const map = TableMap.get($cell.node(-2)), + start = $cell.start(-2); const index = map.map.indexOf($cell.pos - start); return index % map.width == 0 ? -1 : start + map.map[index - 1]; } @@ -1779,9 +2213,9 @@ function updateHandle(view, value) { } function updateColumnWidth(view, cell, width) { const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1), + const table = $cell.node(-2), map = TableMap.get(table), - start = $cell.start(-1); + start = $cell.start(-2); const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; const tr = view.state.tr; @@ -1802,18 +2236,18 @@ function updateColumnWidth(view, cell, width) { } function displayColumnWidth(view, cell, width, cellMinWidth) { const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1), - start = $cell.start(-1); + const table = $cell.node(-2), + tableStart = $cell.start(-2); const col = - TableMap.get(table).colCount($cell.pos - start) + + TableMap.get(table).colCount($cell.pos - tableStart) + $cell.nodeAfter.attrs.colspan - 1; - let dom = view.domAtPos($cell.start(-1)).node; + let dom = view.domAtPos($cell.start(-2)).node; while (dom && dom.nodeName != 'TABLE') { dom = dom.parentNode; } if (!dom) return; - updateColumnsOnResize(table, dom.firstChild, dom, cellMinWidth, col, width); + updateColumnsOnResize(view, table, tableStart, cellMinWidth, col, width); } function zeroes(n) { return Array(n).fill(0); @@ -1821,18 +2255,18 @@ function zeroes(n) { function handleDecorations(state, cell) { const decorations = []; const $cell = state.doc.resolve(cell); - const table = $cell.node(-1); + const table = $cell.node(-2); if (!table) { - return DecorationSet2.empty; + return []; } const map = TableMap.get(table); - const start = $cell.start(-1); + const start = $cell.start(-2); const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan; for (let row = 0; row < map.height; row++) { const index = col + row * map.width - 1; if ( (col == map.width || map.map[index] != map.map[index + 1]) && - (row == 0 || map.map[index] != map.map[index - map.width]) + (row == 0 || map.map[index - 1] != map.map[index - 1 - map.width]) ) { const cellPos = map.map[index]; const pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1; @@ -1841,17 +2275,136 @@ function handleDecorations(state, cell) { decorations.push(Decoration2.widget(pos, dom)); } } - return DecorationSet2.create(state.doc, decorations); + return decorations; +} +function colgroupDecoration(tableStart, colWidths) { + return Decoration2.widget( + tableStart, + (view, getPos) => { + const colgroup = document.createElement('colgroup'); + for (let c = 0; c < colWidths.length; c++) { + const colElement = document.createElement('col'); + colElement.style.width = colWidths[c]; + colgroup.appendChild(colElement); + } + return colgroup; + }, + { + type: SPEC_COL_WIDTHS, + colWidths, + }, + ); +} +function tableWidthDecoration(from, to, css) { + const style = Object.entries(css) + .map(([prop, value]) => `${prop}: ${value}`) + .join('; '); + return Decoration2.node(from, to, { style }, { type: SPEC_TABLE_WIDTH }); +} +function tableDecorationsCallback(doc, decos, cellMinWidth) { + return (node, pos) => { + if (node.type.spec.tableRole === 'table') { + const tableStart = pos + 1; + const resolved = doc.resolve(tableStart); + decos.push(tableWidthDecoration(resolved.before(), resolved.after(), {})); + const { colWidths } = updateColumnsOnResize( + null, + node, + tableStart, + cellMinWidth, + ); + decos.push(colgroupDecoration(tableStart, colWidths)); + return false; + } + return true; + }; +} +function createTableDecorations(doc, cellMinWidth = 0, from, to) { + let decos = []; + if (from && to) + doc.nodesBetween( + from, + to, + tableDecorationsCallback(doc, decos, cellMinWidth), + ); + else doc.descendants(tableDecorationsCallback(doc, decos, cellMinWidth)); + return decos; +} +function updateColumnsOnResize( + view, + table, + tableStart, + cellMinWidth, + overrideCol, + overrideValue, +) { + const { setColWidths, setTableWidth } = getTableWidths( + table, + tableStart, + cellMinWidth, + overrideCol, + overrideValue, + ); + const colWidths = setColWidths[0].colWidths; + const tableWidth = setTableWidth[0].width + 'px'; + if (view) { + view.dispatch( + view.state.tr.setMeta(columnResizingPluginKey, { + setColWidths, + setTableWidth, + }), + ); + } + return { colWidths, tableWidth }; +} +function getCellMinWidth(state) { + const plugin = columnResizingPluginKey.get(state); + return (plugin && plugin.spec.options.cellMinWidth) || 25; +} +function getTableWidths( + table, + tableStart, + cellMinWidth, + overrideCol, + overrideValue, +) { + let totalWidth = 0; + let fixedWidth = true; + const row = getRow(table, 0).node; + if (!row) return; + const colWidths = []; + for (let i = 0, col = 0; i < row.childCount; i++) { + const { colspan, colwidth } = row.child(i).attrs; + for (let j = 0; j < colspan; j++, col++) { + const hasWidth = + overrideCol == col ? overrideValue : colwidth && colwidth[j]; + colWidths.push(hasWidth ? hasWidth + 'px' : ''); + totalWidth += hasWidth || cellMinWidth; + if (!hasWidth) fixedWidth = false; + } + } + const setColWidths = [{ tableStart, colWidths }]; + const pos = tableStart - 1; + const tableWidth = totalWidth + 'px'; + const setTableWidth = [ + fixedWidth + ? { pos, width: totalWidth, css: { 'min-width': '', width: tableWidth } } + : { pos, width: totalWidth, css: { 'min-width': tableWidth, width: '' } }, + ]; + return { setColWidths, setTableWidth }; } // src/commands.ts import { Fragment as Fragment4 } from 'prosemirror-model'; -import { TextSelection as TextSelection3 } from 'prosemirror-state'; +import { + NodeSelection as NodeSelection4, + TextSelection as TextSelection3, +} from 'prosemirror-state'; function selectedRect(state) { const sel = state.selection; const $pos = selectionCell(state); - const table = $pos.node(-1); - const tableStart = $pos.start(-1); + const table = $pos.node(-2); + const tableStart = $pos.start(-2); const map = TableMap.get(table); const rect = sel instanceof CellSelection @@ -1889,7 +2442,7 @@ function addColumn(tr, { map, tableStart, table }, col) { } return tr; } -function addColumnBefore(state, dispatch) { +function addColumnBefore(state, dispatch, view) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); @@ -1897,7 +2450,7 @@ function addColumnBefore(state, dispatch) { } return true; } -function addColumnAfter(state, dispatch) { +function addColumnAfter(state, dispatch, view) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); @@ -1928,7 +2481,7 @@ function removeColumn(tr, { map, table, tableStart }, col) { row += attrs.rowspan; } } -function deleteColumn(state, dispatch) { +function deleteColumn(state, dispatch, view) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); @@ -1962,14 +2515,22 @@ function rowIsHeader(map, table, row) { return false; return true; } -function addRow(tr, { map, tableStart, table }, row) { +function addRow(tr, { bottom, map, tableStart, table }, row) { var _a; - let rowPos = tableStart; - for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; + let rPos = rowPos(table, row) + tableStart; + if (bottom === row && isRowLastInSection(table, row - 1)) rPos -= 2; const cells = []; let refRow = row > 0 ? -1 : 0; if (rowIsHeader(map, table, row + refRow)) refRow = row == 0 || row == map.height ? null : 0; + const srows = map.sectionRows; + for (let s = 0, acc = 0; s < srows.length; s++) { + acc += srows[s]; + if (row < acc || s === srows.length - 1) { + srows[s]++; + break; + } + } for (let col = 0, index = map.width * row; col < map.width; col++, index++) { if ( row > 0 && @@ -1994,7 +2555,7 @@ function addRow(tr, { map, tableStart, table }, row) { if (node) cells.push(node); } } - tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells)); + tr.insert(rPos, tableNodeTypes(table.type.schema).row.create(null, cells)); return tr; } function addRowBefore(state, dispatch) { @@ -2014,11 +2575,11 @@ function addRowAfter(state, dispatch) { return true; } function removeRow(tr, { map, table, tableStart }, row) { - let rowPos = 0; - for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; - const nextRow = rowPos + table.child(row).nodeSize; + const { node: rNode, pos: rPos } = getRow(table, row); const mapFrom = tr.mapping.maps.length; - tr.delete(rowPos + tableStart, nextRow + tableStart); + const from = rPos + tableStart; + const to = from + rNode.nodeSize - 1; + tr.delete(from, to); for (let col = 0, index = row * map.width; col < map.width; col++, index++) { const pos = map.map[index]; if (row > 0 && pos == map.map[index - map.width]) { @@ -2041,15 +2602,43 @@ function removeRow(tr, { map, table, tableStart }, row) { } } } +function removeSection(tr, { map, table, tableStart }, section) { + let pos = 0; + let s = -1; + for (let i = 0; i < table.childCount; i++) { + const child = table.child(i); + if (isTableSection(child)) { + s++; + if (s == section) { + tr.delete(tableStart + pos, tableStart + pos + child.nodeSize); + return; + } + } + pos += child.nodeSize; + } +} function deleteRow(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state), tr = state.tr; if (rect.top == 0 && rect.bottom == rect.map.height) return false; + const sectionRows = rect.map.sectionRows; + const sectionBottom = [sectionRows[0] || 0]; + for (let s2 = 1; s2 < sectionRows.length; s2++) + sectionBottom[s2] = sectionBottom[s2 - 1] + sectionRows[s2]; + let s = sectionRows.length - 1; + while (s > 0 && sectionBottom[s] > rect.bottom) s--; for (let i = rect.bottom - 1; ; i--) { - removeRow(tr, rect, i); - if (i == rect.top) break; + const firstRowOfSection = sectionBottom[s] - sectionRows[s]; + if (i + 1 === sectionBottom[s] && rect.top <= firstRowOfSection) { + removeSection(tr, rect, s); + i = firstRowOfSection; + s--; + } else { + removeRow(tr, rect, i); + } + if (i <= rect.top) break; const table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc; @@ -2063,6 +2652,257 @@ function deleteRow(state, dispatch) { } return true; } +function addCaption(state, dispatch) { + const $anchor = state.selection.$anchor; + const d = tableDepth($anchor); + if (d < 0) return false; + const table = $anchor.node(d); + if (tableHasCaption(table)) return false; + if (dispatch) { + let pos = $anchor.start(d); + const types = tableNodeTypes(state.schema); + const caption = types.caption.createAndFill(); + dispatch(state.tr.insert(pos, caption)); + } + return true; +} +function deleteCaption(state, dispatch) { + const $anchor = state.selection.$anchor; + const d = tableDepth($anchor); + if (d < 0) return false; + const table = $anchor.node(d); + if (!tableHasCaption(table)) return false; + if (dispatch) { + let pos = $anchor.start(d); + const size = table.firstChild.nodeSize; + dispatch(state.tr.delete(pos, pos + size)); + } + return true; +} +function createSection(schema, role, width, cellRole) { + const types = tableNodeTypes(schema); + const cells = []; + const cellType = + (cellRole && types[cellRole]) || types.cell || types.header_cell; + for (let i = 0; i < width; i++) cells.push(cellType.createAndFill()); + return types[role].createAndFill(null, types.row.createAndFill(null, cells)); +} +function addTableHead(state, dispatch) { + const $anchor = state.selection.$anchor; + const d = tableDepth($anchor); + if (d < 0) return false; + const table = $anchor.node(d); + if (tableHasHead(table)) return false; + if (dispatch) { + let pos = $anchor.start(d); + const firstChild = table.child(0); + if (firstChild && firstChild.type.spec.tableRole === 'caption') + pos += firstChild.nodeSize; + const map = TableMap.get(table); + const head = createSection(state.schema, 'head', map.width, 'header_cell'); + dispatch(state.tr.insert(pos, head)); + } + return true; +} +function addTableFoot(state, dispatch) { + const $anchor = state.selection.$anchor; + const d = tableDepth($anchor); + if (d < 0) return false; + const table = $anchor.node(d); + if (tableHasFoot(table)) return false; + if (dispatch) { + const pos = $anchor.end(d); + const map = TableMap.get(table); + const foot = createSection(state.schema, 'foot', map.width, 'header_cell'); + dispatch(state.tr.insert(pos, foot)); + } + return true; +} +function addBodyBefore(state, dispatch) { + if (!isInTable(state)) return false; + const rect = selectedRect(state); + const { map, table, tableStart } = rect; + const firstSection = map.sectionsInRect(rect)[0]; + if (firstSection === void 0 || (firstSection === 0 && tableHasHead(table))) + return false; + if (dispatch) { + let pos = tableStart, + s = -1; + for (let i = 0; i < table.childCount; i++) { + const child = table.child(i); + if (child.type.spec.tableRole != 'caption') s++; + if (s === firstSection) break; + pos += child.nodeSize; + } + const map2 = TableMap.get(table); + const body = createSection(state.schema, 'body', map2.width); + dispatch(state.tr.insert(pos, body)); + } + return true; +} +function addBodyAfter(state, dispatch) { + if (!isInTable(state)) return false; + const rect = selectedRect(state); + const { map, table, tableStart } = rect; + const sections = map.sectionsInRect(rect); + const lastSection = sections[sections.length - 1]; + if (lastSection === map.sectionRows.length - 1 && tableHasFoot(table)) + return false; + if (dispatch) { + let pos = tableStart - 1, + s = -1; + for (let i = 0; i < table.childCount; i++) { + const child = table.child(i); + pos += child.nodeSize; + if (child.type.spec.tableRole != 'caption') s++; + if (s === lastSection) break; + } + const map2 = TableMap.get(table); + const body = createSection(state.schema, 'body', map2.width); + dispatch(state.tr.insert(pos, body)); + } + return true; +} +function fixRowCells(row, headerCellType) { + const newCells = []; + for (let i = 0; i < row.childCount; i++) { + const cell = row.child(i); + newCells.push( + cell.type.spec.tableRole === 'header_cell' + ? cell + : headerCellType.create(cell.attrs, cell.content), + ); + } + return row.copy(Fragment4.from(newCells)); +} +function makeSection(role, state, dispatch) { + if (!isInTable(state)) return false; + const rect = selectedRect(state); + const { map, table, tableStart, top, bottom } = rect; + if (role === 'head' && top > 0) return false; + if (role === 'foot' && bottom < map.height) return false; + const tableTypes = tableNodeTypes(state.schema); + const newSectionType = tableTypes[role]; + if (!newSectionType) return false; + const fixCellsType = + (role === 'head' || role === 'foot') && + tableTypes.cell && + tableTypes.header_cell; + if (dispatch) { + let newTableContents = Fragment4.empty; + let refSection = null; + let rowIndex = 0; + let inSelection = false; + let accSectionRows = Fragment4.empty; + for (let i = 0; i < table.childCount; i++) { + const section = table.child(i); + const sectionRole = section.type.spec.tableRole; + if (isTableSection(section)) { + const sectionRowsCount = section.childCount; + const lastRow = rowIndex + sectionRowsCount - 1; + if ( + rowIndex === top && + lastRow + 1 === bottom && + sectionRole === role + ) { + return false; + } + if (rowIndex >= bottom || lastRow < top) { + newTableContents = newTableContents.addToEnd(section); + } else { + if (!refSection) refSection = section; + for (let j = 0; j < section.childCount; j++) { + if (rowIndex + j === top) { + if (accSectionRows.childCount > 0) { + newTableContents = newTableContents.addToEnd( + refSection.copy(accSectionRows), + ); + accSectionRows = Fragment4.empty; + } + inSelection = true; + } + const row = + inSelection && fixCellsType + ? fixRowCells(section.child(j), tableTypes.header_cell) + : section.child(j); + accSectionRows = accSectionRows.addToEnd(row); + if (rowIndex + j === bottom - 1) { + if (refSection.type.spec.tableRole !== role) refSection = section; + const newSection = + refSection.type.spec.tableRole !== role + ? newSectionType.create(null, accSectionRows) + : refSection.copy(accSectionRows); + newTableContents = newTableContents.addToEnd(newSection); + accSectionRows = Fragment4.empty; + refSection = section; + inSelection = false; + } + } + if (!inSelection && accSectionRows.childCount > 0) { + newTableContents = newTableContents.addToEnd( + refSection.copy(accSectionRows), + ); + accSectionRows = Fragment4.empty; + } + } + rowIndex = lastRow + 1; + } else { + newTableContents = newTableContents.addToEnd(section); + } + } + const { doc, tr } = state; + tr.setSelection(new NodeSelection4(doc.resolve(tableStart - 1))); + const newTable = table.copy(newTableContents); + tr.replaceSelectionWith(newTable); + const cellsPositions = TableMap.get(newTable).cellsInRect(rect); + const $anchorCell = tr.doc.resolve(tableStart + cellsPositions[0]); + const $headCell = tr.doc.resolve( + tableStart + cellsPositions[cellsPositions.length - 1], + ); + tr.setSelection(new CellSelection($anchorCell, $headCell)); + tr.setMeta( + columnResizingPluginKey, + getTableWidths(table, tableStart, getCellMinWidth(state)), + ); + dispatch(tr); + } + return true; +} +function makeBody(state, dispatch) { + return makeSection('body', state, dispatch); +} +function makeHead(state, dispatch) { + return makeSection('head', state, dispatch); +} +function makeFoot(state, dispatch) { + return makeSection('foot', state, dispatch); +} +function deleteSection(state, dispatch) { + if (!isInTable(state)) return false; + const rect = selectedRect(state), + tr = state.tr; + if (rect.top == 0 && rect.bottom == rect.map.height) return false; + if (dispatch) { + const { map, table, tableStart } = rect; + const sections = map.sectionsInRect(rect); + if (sections.length >= tableSectionsCount(table) || sections.length == 0) + return false; + const firstSectionIndex = tableHasCaption(table) ? 1 : 0; + const sectionPosAndSize = []; + let pos = tableStart; + for (let i = 0; i < table.childCount; i++) { + const size = table.child(i).nodeSize; + if (i >= firstSectionIndex) sectionPosAndSize.push([pos, size]); + pos += size; + } + for (let i = sections.length - 1; i >= 0; i--) { + const [pos2, size] = sectionPosAndSize[sections[i]]; + tr.delete(pos2, pos2 + size); + } + dispatch(tr); + } + return true; +} function isEmpty(cell) { const c = cell.content; return ( @@ -2103,6 +2943,7 @@ function mergeCells(state, dispatch) { return false; const rect = selectedRect(state), { map } = rect; + if (!map.rectOverOneSection(rect)) return false; if (cellsOverlapRectangle(map, rect)) return false; if (dispatch) { const tr = state.tr; @@ -2376,15 +3217,19 @@ var toggleHeaderCell = toggleHeader('cell', { useDeprecatedLogic: true, }); function findNextCell($cell, dir) { + const table = $cell.node(-2); + const tableStart = $cell.start(-2); if (dir < 0) { const before = $cell.nodeBefore; - if (before) return $cell.pos - before.nodeSize; + if (before) { + return $cell.pos - before.nodeSize; + } for ( - let row = $cell.index(-1) - 1, rowEnd = $cell.before(); + let row = $cell.index(-2) - 1, rowEnd = $cell.before(); row >= 0; row-- ) { - const rowNode = $cell.node(-1).child(row); + const rowNode = $cell.node(-2).child(row); const lastChild = rowNode.lastChild; if (lastChild) { return rowEnd - 1 - lastChild.nodeSize; @@ -2395,9 +3240,8 @@ function findNextCell($cell, dir) { if ($cell.index() < $cell.parent.childCount - 1) { return $cell.pos + $cell.nodeAfter.nodeSize; } - const table = $cell.node(-1); for ( - let row = $cell.indexAfter(-1), rowStart = $cell.after(); + let row = $cell.indexAfter(-2), rowStart = $cell.after(); row < table.childCount; row++ ) { @@ -2486,10 +3330,12 @@ export { CellSelection, ResizeState, TableMap, - TableView, clipCells as __clipCells, insertCells as __insertCells, pastedCells as __pastedCells, + addBodyAfter, + addBodyBefore, + addCaption, addColSpan, addColumn, addColumnAfter, @@ -2497,21 +3343,30 @@ export { addRow, addRowAfter, addRowBefore, + addTableFoot, + addTableHead, cellAround, colCount, columnIsHeader, columnResizing, columnResizingPluginKey, + deleteCaption, deleteColumn, deleteRow, + deleteSection, deleteTable, findCell, fixTables, fixTablesKey, + getRow, goToNextCell, handlePaste, inSameTable, isInTable, + isRowLastInSection, + makeBody, + makeFoot, + makeHead, mergeCells, moveCellForward, nextCell, @@ -2519,19 +3374,27 @@ export { removeColSpan, removeColumn, removeRow, + removeSection, + rowAtPos, rowIsHeader, + rowPos, + rowsCount, selectedRect, selectionCell, setCellAttr, splitCell, splitCellWithType, + tableBodiesCount, tableEditing, tableEditingKey, + tableHasCaption, + tableHasFoot, + tableHasHead, tableNodeTypes, tableNodes, + tableSectionsCount, toggleHeader, toggleHeaderCell, toggleHeaderColumn, toggleHeaderRow, - updateColumnsOnResize, };