-
-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support spanning cell over rows/columns (#188)
- Loading branch information
Showing
47 changed files
with
2,071 additions
and
546 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
##### config.spanningCells | ||
|
||
Type: `SpanningCellConfig[]` | ||
|
||
Spanning cells configuration. | ||
|
||
The configuration should be straightforward: just specify an array of minimal cell configurations including the position of top-left cell | ||
and the number of columns and/or rows will be expanded from it. | ||
|
||
The content of overlap cells will be ignored to make the `data` shape be consistent. | ||
|
||
By default, the configuration of column that the top-left cell belongs to will be applied to the whole spanning cell, except: | ||
* The `width` will be summed up of all spanning columns. | ||
* The `paddingRight` will be received from the right-most column intentionally. | ||
|
||
Advances customized column-like styles can be configurable to each spanning cell to overwrite the default behavior. | ||
|
||
```js | ||
const data = [ | ||
['Test Coverage Report', '', '', '', '', ''], | ||
['Module', 'Component', 'Test Cases', 'Failures', 'Durations', 'Success Rate'], | ||
['Services', 'User', '50', '30', '3m 7s', '60.0%'], | ||
['', 'Payment', '100', '80', '7m 15s', '80.0%'], | ||
['Subtotal', '', '150', '110', '10m 22s', '73.3%'], | ||
['Controllers', 'User', '24', '18', '1m 30s', '75.0%'], | ||
['', 'Payment', '30', '24', '50s', '80.0%'], | ||
['Subtotal', '', '54', '42', '2m 20s', '77.8%'], | ||
['Total', '', '204', '152', '12m 42s', '74.5%'], | ||
]; | ||
|
||
const config = { | ||
columns: [ | ||
{ alignment: 'center', width: 12 }, | ||
{ alignment: 'center', width: 10 }, | ||
{ alignment: 'right' }, | ||
{ alignment: 'right' }, | ||
{ alignment: 'right' }, | ||
{ alignment: 'right' } | ||
], | ||
spanningCells: [ | ||
{ col: 0, row: 0, colSpan: 6 }, | ||
{ col: 0, row: 2, rowSpan: 2, verticalAlignment: 'middle'}, | ||
{ col: 0, row: 4, colSpan: 2, alignment: 'right'}, | ||
{ col: 0, row: 5, rowSpan: 2, verticalAlignment: 'middle'}, | ||
{ col: 0, row: 7, colSpan: 2, alignment: 'right' }, | ||
{ col: 0, row: 8, colSpan: 2, alignment: 'right' } | ||
], | ||
}; | ||
|
||
console.log(table(data, config)); | ||
``` | ||
|
||
``` | ||
╔══════════════════════════════════════════════════════════════════════════════╗ | ||
║ Test Coverage Report ║ | ||
╟──────────────┬────────────┬────────────┬──────────┬───────────┬──────────────╢ | ||
║ Module │ Component │ Test Cases │ Failures │ Durations │ Success Rate ║ | ||
╟──────────────┼────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ │ User │ 50 │ 30 │ 3m 7s │ 60.0% ║ | ||
║ Services ├────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ │ Payment │ 100 │ 80 │ 7m 15s │ 80.0% ║ | ||
╟──────────────┴────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ Subtotal │ 150 │ 110 │ 10m 22s │ 73.3% ║ | ||
╟──────────────┬────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ │ User │ 24 │ 18 │ 1m 30s │ 75.0% ║ | ||
║ Controllers ├────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ │ Payment │ 30 │ 24 │ 50s │ 80.0% ║ | ||
╟──────────────┴────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ Subtotal │ 54 │ 42 │ 2m 20s │ 77.8% ║ | ||
╟───────────────────────────┼────────────┼──────────┼───────────┼──────────────╢ | ||
║ Total │ 204 │ 152 │ 12m 42s │ 74.5% ║ | ||
╚═══════════════════════════╧════════════╧══════════╧═══════════╧══════════════╝ | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { | ||
alignString, | ||
} from './alignString'; | ||
import { | ||
padCellVertically, | ||
} from './mapDataUsingRowHeights'; | ||
import { | ||
padString, | ||
} from './padTableData'; | ||
import type { | ||
SpanningCellContext, | ||
} from './spanningCellManager'; | ||
import { | ||
truncateString, | ||
} from './truncateTableData'; | ||
import type { | ||
RangeConfig, | ||
} from './types/internal'; | ||
import { | ||
sequence, sumArray, | ||
} from './utils'; | ||
import { | ||
wrapCell, | ||
} from './wrapCell'; | ||
|
||
/** | ||
* Fill content into all cells in range in order to calculate total height | ||
*/ | ||
export const wrapRangeContent = (rangeConfig: RangeConfig, rangeWidth: number, context: SpanningCellContext): string[] => { | ||
const {topLeft, paddingRight, paddingLeft, truncate, wrapWord, alignment} = rangeConfig; | ||
|
||
const originalContent = context.rows[topLeft.row][topLeft.col]; | ||
const contentWidth = rangeWidth - paddingLeft - paddingRight; | ||
|
||
return wrapCell(truncateString(originalContent, truncate), contentWidth, wrapWord).map((line) => { | ||
const alignedLine = alignString(line, contentWidth, alignment); | ||
|
||
return padString(alignedLine, paddingLeft, paddingRight); | ||
}); | ||
}; | ||
|
||
export const alignVerticalRangeContent = (range: RangeConfig, content: string[], context: SpanningCellContext) => { | ||
const {rows, drawHorizontalLine, rowHeights} = context; | ||
const {topLeft, bottomRight, verticalAlignment} = range; | ||
|
||
// They are empty before calculateRowHeights function run | ||
if (rowHeights.length === 0) { | ||
return []; | ||
} | ||
|
||
const totalCellHeight = sumArray(rowHeights.slice(topLeft.row, bottomRight.row + 1)); | ||
const totalBorderHeight = bottomRight.row - topLeft.row; | ||
const hiddenHorizontalBorderCount = sequence(topLeft.row + 1, bottomRight.row).filter((horizontalBorderIndex) => { | ||
return !drawHorizontalLine(horizontalBorderIndex, rows.length); | ||
}).length; | ||
|
||
const availableRangeHeight = totalCellHeight + totalBorderHeight - hiddenHorizontalBorderCount; | ||
|
||
return padCellVertically(content, availableRangeHeight, verticalAlignment).map((line) => { | ||
if (line.length === 0) { | ||
return ' '.repeat(content[0].length); | ||
} | ||
|
||
return line; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import stringWidth from 'string-width'; | ||
import type { | ||
SpanningCellConfig, | ||
} from './types/api'; | ||
import type { | ||
Row, | ||
Cell, | ||
} from './types/internal'; | ||
import { | ||
calculateRangeCoordinate, isCellInRange, | ||
} from './utils'; | ||
|
||
export const calculateMaximumCellWidth = (cell: Cell): number => { | ||
return Math.max( | ||
...cell.split('\n').map(stringWidth), | ||
); | ||
}; | ||
|
||
/** | ||
* Produces an array of values that describe the largest value length (width) in every column. | ||
*/ | ||
export const calculateMaximumColumnWidths = (rows: Row[], spanningCellConfigs: SpanningCellConfig[] = []): number[] => { | ||
const columnWidths = new Array(rows[0].length).fill(0); | ||
const rangeCoordinates = spanningCellConfigs.map(calculateRangeCoordinate); | ||
const isSpanningCell = (rowIndex: number, columnIndex: number): boolean => { | ||
return rangeCoordinates.some((rangeCoordinate) => { | ||
return isCellInRange({col: columnIndex, | ||
row: rowIndex}, rangeCoordinate); | ||
}); | ||
}; | ||
|
||
rows.forEach((row, rowIndex) => { | ||
row.forEach((cell, cellIndex) => { | ||
if (isSpanningCell(rowIndex, cellIndex)) { | ||
return; | ||
} | ||
columnWidths[cellIndex] = Math.max(columnWidths[cellIndex], calculateMaximumCellWidth(cell)); | ||
}); | ||
}); | ||
|
||
return columnWidths; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { | ||
TableConfig, | ||
} from './types/internal'; | ||
|
||
export const calculateOutputColumnWidths = (config: TableConfig): number[] => { | ||
return config.columns.map((col) => { | ||
return col.paddingLeft + col.width + col.paddingRight; | ||
}); | ||
}; |
Oops, something went wrong.