From ae12e5b900759e5d1411db629b42f084867e5687 Mon Sep 17 00:00:00 2001 From: Dobromir Hristov Date: Wed, 10 Aug 2022 19:14:47 +0300 Subject: [PATCH 1/7] feat: add table cell merging --- src/components/ContentNode.vue | 91 ++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/src/components/ContentNode.vue b/src/components/ContentNode.vue index cfee850d9..e341850f8 100644 --- a/src/components/ContentNode.vue +++ b/src/components/ContentNode.vue @@ -100,7 +100,42 @@ function renderNode(createElement, references) { )) )); - const renderTableChildren = (rows, headerStyle = TableHeaderStyle.none) => { + const renderTableCell = ( + element, attrs, data, cellIndex, rowIndex, extendedData, cellsToRender, + ) => { + if (!cellsToRender[rowIndex][cellIndex]) return null; + const { colspan, rowspan } = extendedData[`${rowIndex}_${cellIndex}`] || {}; + return createElement(element, { attrs: { ...attrs, colspan, rowspan } }, ( + renderChildren(data) + )); + }; + + function buildTableVisibilityMatrix(rows, extendedData) { + const matrix = Array(rows.length).fill(1).map(() => Array(rows[0].length).fill(1)); + // specify which cells should be skipped, when siblings span across them. + Object.entries(extendedData).forEach(([key, { rowspan, colspan }]) => { + const split = key.split('_'); + const row = parseInt(split[0], 10); + const col = parseInt(split[1], 10); + if (colspan) { + // replace the cols in the row with '0' values, indicating they should be skipped + const cellCount = colspan - 1; + matrix[row].splice(col + 1, cellCount, ...Array(cellCount).fill(0)); + } + if (rowspan) { + // iterate over the rows that that need changing + let i; + for (i = row + 1; i > row && i < row + rowspan; i += 1) { + matrix[i].splice(col, 1, 0); + } + } + }); + return matrix; + } + + const renderTableChildren = (rows, headerStyle = TableHeaderStyle.none, extendedData = {}) => { + // build the matrix for the array + const tableMatrix = buildTableVisibilityMatrix(rows, extendedData); switch (headerStyle) { // thead with first row and th for each first row cell // tbody with rows where first cell in each row is th, others are td @@ -108,21 +143,15 @@ function renderNode(createElement, references) { const [firstRow, ...otherRows] = rows; return [ createElement('thead', {}, [ - createElement('tr', {}, firstRow.map(cell => ( - createElement('th', { attrs: { scope: 'col' } }, ( - renderChildren(cell) - )) + createElement('tr', {}, firstRow.map((cell, ci) => ( + renderTableCell('th', { scope: 'col' }, cell, ci, 0, extendedData, tableMatrix) ))), ]), - createElement('tbody', {}, otherRows.map(([firstCell, ...otherCells]) => ( + createElement('tbody', {}, otherRows.map(([firstCell, ...otherCells], ri) => ( createElement('tr', {}, [ - createElement('th', { attrs: { scope: 'row' } }, ( - renderChildren(firstCell) - )), - ...otherCells.map(cell => ( - createElement('td', {}, ( - renderChildren(cell) - )) + renderTableCell('th', { scope: 'row' }, firstCell, 0, ri + 1, extendedData, tableMatrix), + ...otherCells.map((cell, ci) => ( + renderTableCell('td', {}, cell, ci + 1, ri + 1, extendedData, tableMatrix) )), ]) ))), @@ -131,15 +160,11 @@ function renderNode(createElement, references) { // tbody with rows, th for first cell of each row, td for other cells case TableHeaderStyle.column: return [ - createElement('tbody', {}, rows.map(([firstCell, ...otherCells]) => ( + createElement('tbody', {}, rows.map(([firstCell, ...otherCells], ri) => ( createElement('tr', {}, [ - createElement('th', { attrs: { scope: 'row' } }, ( - renderChildren(firstCell) - )), - ...otherCells.map(cell => ( - createElement('td', {}, ( - renderChildren(cell) - )) + renderTableCell('th', { scope: 'row' }, firstCell, 0, ri, extendedData, tableMatrix), + ...otherCells.map((cell, ci) => ( + renderTableCell('td', {}, cell, ci + 1, ri, extendedData, tableMatrix) )), ]) ))), @@ -150,17 +175,13 @@ function renderNode(createElement, references) { const [firstRow, ...otherRows] = rows; return [ createElement('thead', {}, [ - createElement('tr', {}, firstRow.map(cell => ( - createElement('th', { attrs: { scope: 'col' } }, ( - renderChildren(cell) - )) + createElement('tr', {}, firstRow.map((cell, ci) => renderTableCell( + 'th', { scope: 'col' }, cell, ci, 0, extendedData, tableMatrix, ))), ]), - createElement('tbody', {}, otherRows.map(row => ( - createElement('tr', {}, row.map(cell => ( - createElement('td', {}, ( - renderChildren(cell) - )) + createElement('tbody', {}, otherRows.map((row, ri) => ( + createElement('tr', {}, row.map((cell, cellIndex) => ( + renderTableCell('td', {}, cell, cellIndex, ri + 1, extendedData, tableMatrix) ))) ))), ]; @@ -169,12 +190,10 @@ function renderNode(createElement, references) { // tbody with all rows and every cell is td return [ createElement('tbody', {}, ( - rows.map(row => ( + rows.map((row, ri) => ( createElement('tr', {}, ( - row.map(cell => ( - createElement('td', {}, ( - renderChildren(cell) - )) + row.map((cell, ci) => ( + renderTableCell('td', {}, cell, ci, ri, extendedData, tableMatrix) )) )) )) @@ -253,7 +272,7 @@ function renderNode(createElement, references) { } return createElement(Table, {}, ( - renderTableChildren(node.rows, node.header) + renderTableChildren(node.rows, node.header, node.extendedData) )); case BlockType.termList: return createElement('dl', {}, node.items.map(({ term, definition }) => [ From 5988fc253b97a0d57e66c2123e9fab98e4eadc8b Mon Sep 17 00:00:00 2001 From: Dobromir Hristov Date: Fri, 12 Aug 2022 13:24:00 +0300 Subject: [PATCH 2/7] chore: add tests --- tests/unit/components/ContentNode.spec.js | 285 +++++++++++++++++----- 1 file changed, 222 insertions(+), 63 deletions(-) diff --git a/tests/unit/components/ContentNode.spec.js b/tests/unit/components/ContentNode.spec.js index 2cdaee03e..1b5d39e93 100644 --- a/tests/unit/components/ContentNode.spec.js +++ b/tests/unit/components/ContentNode.spec.js @@ -875,75 +875,234 @@ describe('ContentNode', () => { expect(caption.text()).toContain('blah'); }); - describe('with type="termList"', () => { - it('renders a
with
/
pairs for each term/definition', () => { + describe('and column/row spanning', () => { + // + // + // + // + // + // + // + // + // + // + // + // + // + // + //
row0col0row0col2
row1col0row1col1row1col2
row2col0row2col1
+ const rowsExtended = [ + [ + [{ type: 'text', text: 'row0col0' }], + [{ type: 'text', text: 'row0col1' }], + [{ type: 'text', text: 'row0col2' }], + ], + [ + [{ type: 'text', text: 'row1col0' }], + [{ type: 'text', text: 'row1col1' }], + [{ type: 'text', text: 'row1col2' }], + ], + [ + [{ type: 'text', text: 'row2col0' }], + [{ type: 'text', text: 'row2col1' }], + [{ type: 'text', text: 'row2col2' }], + ], + ]; + const extendedData = { + '0_0': { colspan: 2 }, + '1_2': { rowspan: 2 }, + }; + + it('renders header="none" style table, with spans', () => { const wrapper = mountWithItem({ - type: 'termList', - items: [ - { - term: { - inlineContent: [ - { - type: 'text', - text: 'Foo', - }, - ], - }, - definition: { - content: [ - { - type: 'paragraph', - inlineContent: [ - { - type: 'text', - text: 'foo', - }, - ], - }, - ], - }, - }, - { - term: { - inlineContent: [ - { - type: 'text', - text: 'Bar', - }, - ], - }, - definition: { - content: [ - { - type: 'paragraph', - inlineContent: [ - { - type: 'text', - text: 'bar', - }, - ], - }, - ], - }, - }, - ], + type: 'table', + header: TableHeaderStyle.none, + rows: rowsExtended, + extendedData, + }); + const table = wrapper.find('.content').find(Table); + expect(table.html()).toMatchInlineSnapshot(` + + + + row0col0 + row0col2 + + + row1col0 + row1col1 + row1col2 + + + row2col0 + row2col1 + + + + `); + }); + + it('renders header="both" style table, with spans', () => { + const wrapper = mountWithItem({ + type: 'table', + header: TableHeaderStyle.both, + rows: rowsExtended, + extendedData, }); - const dl = wrapper.find('.content dl'); - expect(dl.exists()).toBe(true); + const table = wrapper.find('.content').find(Table); + expect(table.html()).toMatchInlineSnapshot(` + + + + row0col0 + row0col2 + + + + + row1col0 + row1col1 + row1col2 + + + row2col0 + row2col1 + + + + `); + }); - const terms = dl.findAll('dt'); - expect(terms.length).toBe(2); + it('renders header="row" style table, with spans', () => { + const wrapper = mountWithItem({ + type: 'table', + header: TableHeaderStyle.row, + rows: rowsExtended, + extendedData, + }); + const table = wrapper.find('.content').find(Table); + expect(table.html()).toMatchInlineSnapshot(` + + + + row0col0 + row0col2 + + + + + row1col0 + row1col1 + row1col2 + + + row2col0 + row2col1 + + + + `); + }); - const definitions = dl.findAll('dd'); - expect(definitions.length).toBe(2); + it('renders header="column" style table, with spans', () => { + const wrapper = mountWithItem({ + type: 'table', + header: TableHeaderStyle.column, + rows: rowsExtended, + extendedData, + }); + const table = wrapper.find('.content').find(Table); + expect(table.html()).toMatchInlineSnapshot(` + + + + row0col0 + row0col2 + + + row1col0 + row1col1 + row1col2 + + + row2col0 + row2col1 + + + + `); + }); + }); + }); - expect(terms.at(0).text()).toBe('Foo'); - expect(definitions.at(0).contains('p')).toBe(true); - expect(definitions.at(0).text()).toBe('foo'); - expect(terms.at(1).text()).toBe('Bar'); - expect(definitions.at(1).contains('p')).toBe(true); - expect(definitions.at(1).text()).toBe('bar'); + describe('with type="termList"', () => { + it('renders a
with
/
pairs for each term/definition', () => { + const wrapper = mountWithItem({ + type: 'termList', + items: [ + { + term: { + inlineContent: [ + { + type: 'text', + text: 'Foo', + }, + ], + }, + definition: { + content: [ + { + type: 'paragraph', + inlineContent: [ + { + type: 'text', + text: 'foo', + }, + ], + }, + ], + }, + }, + { + term: { + inlineContent: [ + { + type: 'text', + text: 'Bar', + }, + ], + }, + definition: { + content: [ + { + type: 'paragraph', + inlineContent: [ + { + type: 'text', + text: 'bar', + }, + ], + }, + ], + }, + }, + ], }); + const dl = wrapper.find('.content dl'); + expect(dl.exists()).toBe(true); + + const terms = dl.findAll('dt'); + expect(terms.length).toBe(2); + + const definitions = dl.findAll('dd'); + expect(definitions.length).toBe(2); + + expect(terms.at(0).text()).toBe('Foo'); + expect(definitions.at(0).contains('p')).toBe(true); + expect(definitions.at(0).text()).toBe('foo'); + expect(terms.at(1).text()).toBe('Bar'); + expect(definitions.at(1).contains('p')).toBe(true); + expect(definitions.at(1).text()).toBe('bar'); }); }); From 3c514d4c0038a1f0373425cc82a3b997fd5b4c6a Mon Sep 17 00:00:00 2001 From: Dobromir Hristov Date: Wed, 24 Aug 2022 12:00:42 +0300 Subject: [PATCH 3/7] refactor: add spanned class for tables that have col/row span --- src/components/ContentNode.vue | 6 ++++- src/components/ContentNode/Table.vue | 26 ++++++++++++++++--- tests/unit/components/ContentNode.spec.js | 9 ++++--- .../unit/components/ContentNode/Table.spec.js | 11 ++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/components/ContentNode.vue b/src/components/ContentNode.vue index e341850f8..90619c9a7 100644 --- a/src/components/ContentNode.vue +++ b/src/components/ContentNode.vue @@ -271,7 +271,11 @@ function renderNode(createElement, references) { return renderFigure(node); } - return createElement(Table, {}, ( + return createElement(Table, { + props: { + spanned: !!node.extendedData, + }, + }, ( renderTableChildren(node.rows, node.header, node.extendedData) )); case BlockType.termList: diff --git a/src/components/ContentNode/Table.vue b/src/components/ContentNode/Table.vue index 965ea7f08..f56b404c1 100644 --- a/src/components/ContentNode/Table.vue +++ b/src/components/ContentNode/Table.vue @@ -9,11 +9,23 @@ -->