| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749 |
- /**
- * PptxGenJS: Table Generation
- */
- import { DEF_FONT_SIZE, DEF_SLIDE_MARGIN_IN, EMU, LINEH_MODIFIER, ONEPT, SLIDE_OBJECT_TYPES } from './core-enums'
- import { PresLayout, SlideLayout, TableCell, TableToSlidesProps, TableRow, TableRowSlide, TableCellProps } from './core-interfaces'
- import { getSmartParseNumber, inch2Emu, rgbToHex, valToPts } from './gen-utils'
- import PptxGenJS from './pptxgen'
- /**
- * Break cell text into lines based upon table column width (e.g.: Magic Happens Here(tm))
- * @param {TableCell} cell - table cell
- * @param {number} colWidth - table column width (inches)
- * @return {TableRow[]} - cell's text objects grouped into lines
- */
- function parseTextToLines(cell: TableCell, colWidth: number, verbose?: boolean): TableCell[][] {
- // FYI: CPL = Width / (font-size / font-constant)
- // FYI: CHAR:2.3, colWidth:10, fontSize:12 => CPL=138, (actual chars per line in PPT)=145 [14.5 CPI]
- // FYI: CHAR:2.3, colWidth:7 , fontSize:12 => CPL= 97, (actual chars per line in PPT)=100 [14.3 CPI]
- // FYI: CHAR:2.3, colWidth:9 , fontSize:16 => CPL= 96, (actual chars per line in PPT)=84 [ 9.3 CPI]
- const FOCO = 2.3 + (cell.options?.autoPageCharWeight ? cell.options.autoPageCharWeight : 0) // Character Constant
- const CPL = Math.floor((colWidth / ONEPT) * EMU) / ((cell.options?.fontSize ? cell.options.fontSize : DEF_FONT_SIZE) / FOCO) // Chars-Per-Line
- const parsedLines: TableCell[][] = []
- let inputCells: TableCell[] = []
- const inputLines1: TableCell[][] = []
- const inputLines2: TableCell[][] = []
- /*
- if (cell.options && cell.options.autoPageCharWeight) {
- let CHR1 = 2.3 + (cell.options && cell.options.autoPageCharWeight ? cell.options.autoPageCharWeight : 0) // Character Constant
- let CPL1 = ((colWidth / ONEPT) * EMU) / ((cell.options && cell.options.fontSize ? cell.options.fontSize : DEF_FONT_SIZE) / CHR1) // Chars-Per-Line
- console.log(`cell.options.autoPageCharWeight: '${cell.options.autoPageCharWeight}' => CPL: ${CPL1}`)
- let CHR2 = 2.3 + 0
- let CPL2 = ((colWidth / ONEPT) * EMU) / ((cell.options && cell.options.fontSize ? cell.options.fontSize : DEF_FONT_SIZE) / CHR2) // Chars-Per-Line
- console.log(`cell.options.autoPageCharWeight: '0' => CPL: ${CPL2}`)
- }
- */
- /**
- * EX INPUTS: `cell.text`
- * - string....: "Account Name Column"
- * - object....: { text:"Account Name Column" }
- * - object[]..: [{ text:"Account Name", options:{ bold:true } }, { text:" Column" }]
- * - object[]..: [{ text:"Account Name", options:{ breakLine:true } }, { text:"Input" }]
- */
- /**
- * EX OUTPUTS:
- * - string....: [{ text:"Account Name Column" }]
- * - object....: [{ text:"Account Name Column" }]
- * - object[]..: [{ text:"Account Name", options:{ breakLine:true } }, { text:"Input" }]
- * - object[]..: [{ text:"Account Name", options:{ breakLine:true } }, { text:"Input" }]
- */
- // STEP 1: Ensure inputCells is an array of TableCells
- if (cell.text && cell.text.toString().trim().length === 0) {
- // Allow a single space/whitespace as cell text (user-requested feature)
- inputCells.push({ _type: SLIDE_OBJECT_TYPES.tablecell, text: ' ' })
- } else if (typeof cell.text === 'number' || typeof cell.text === 'string') {
- inputCells.push({ _type: SLIDE_OBJECT_TYPES.tablecell, text: (cell.text || '').toString().trim() })
- } else if (Array.isArray(cell.text)) {
- inputCells = cell.text
- }
- if (verbose) {
- console.log('[1/4] inputCells')
- inputCells.forEach((cell, idx) => console.log(`[1/4] [${idx + 1}] cell: ${JSON.stringify(cell)}`))
- // console.log('...............................................\n\n')
- }
- // STEP 2: Group table cells into lines based on "\n" or `breakLine` prop
- /**
- * - EX: `[{ text:"Input Output" }, { text:"Extra" }]` == 1 line
- * - EX: `[{ text:"Input" }, { text:"Output", options:{ breakLine:true } }]` == 1 line
- * - EX: `[{ text:"Input\nOutput" }]` == 2 lines
- * - EX: `[{ text:"Input", options:{ breakLine:true } }, { text:"Output" }]` == 2 lines
- */
- let newLine: TableCell[] = []
- inputCells.forEach(cell => {
- // (this is always true, we just constructed them above, but we need to tell typescript b/c type is still string||Cell[])
- if (typeof cell.text === 'string') {
- if (cell.text.split('\n').length > 1) {
- cell.text.split('\n').forEach(textLine => {
- newLine.push({
- _type: SLIDE_OBJECT_TYPES.tablecell,
- text: textLine,
- options: { ...cell.options, ...{ breakLine: true } },
- })
- })
- } else {
- newLine.push({
- _type: SLIDE_OBJECT_TYPES.tablecell,
- text: cell.text.trim(),
- options: cell.options,
- })
- }
- if (cell.options?.breakLine) {
- if (verbose) console.log(`inputCells: new line > ${JSON.stringify(newLine)}`)
- inputLines1.push(newLine)
- newLine = []
- }
- }
- // Flush buffer
- if (newLine.length > 0) {
- inputLines1.push(newLine)
- newLine = []
- }
- })
- if (verbose) {
- console.log(`[2/4] inputLines1 (${inputLines1.length})`)
- inputLines1.forEach((line, idx) => console.log(`[2/4] [${idx + 1}] line: ${JSON.stringify(line)}`))
- // console.log('...............................................\n\n')
- }
- // STEP 3: Tokenize every text object into words (then it's really easy to assemble lines below without having to break text, add its `options`, etc.)
- inputLines1.forEach(line => {
- line.forEach(cell => {
- const lineCells: TableCell[] = []
- const cellTextStr = String(cell.text) // force convert to string (compiled JS is better with this than a cast)
- const lineWords = cellTextStr.split(' ')
- lineWords.forEach((word, idx) => {
- const cellProps = { ...cell.options }
- // IMPORTANT: Handle `breakLine` prop - we cannot apply to each word - only apply to very last word!
- if (cellProps?.breakLine) cellProps.breakLine = idx + 1 === lineWords.length
- lineCells.push({ _type: SLIDE_OBJECT_TYPES.tablecell, text: word + (idx + 1 < lineWords.length ? ' ' : ''), options: cellProps })
- })
- inputLines2.push(lineCells)
- })
- })
- if (verbose) {
- console.log(`[3/4] inputLines2 (${inputLines2.length})`)
- inputLines2.forEach(line => console.log(`[3/4] line: ${JSON.stringify(line)}`))
- // console.log('...............................................\n\n')
- }
- // STEP 4: Group cells/words into lines based upon space consumed by word letters
- inputLines2.forEach(line => {
- let lineCells: TableCell[] = []
- let strCurrLine = ''
- line.forEach(word => {
- // A: create new line when horizontal space is exhausted
- if (strCurrLine.length + word.text.length > CPL) {
- // if (verbose) console.log(`STEP 4: New line added: (${strCurrLine.length} + ${word.text.length} > ${CPL})`);
- parsedLines.push(lineCells)
- lineCells = []
- strCurrLine = ''
- }
- // B: add current word to line cells
- lineCells.push(word)
- // C: add current word to `strCurrLine` which we use to keep track of line's char length
- strCurrLine += word.text.toString()
- })
- // Flush buffer: Only create a line when there's text to avoid empty row
- if (lineCells.length > 0) parsedLines.push(lineCells)
- })
- if (verbose) {
- console.log(`[4/4] parsedLines (${parsedLines.length})`)
- parsedLines.forEach((line, idx) => console.log(`[4/4] [Line ${idx + 1}]:\n${JSON.stringify(line)}`))
- console.log('...............................................\n\n')
- }
- // Done:
- return parsedLines
- }
- /**
- * Takes an array of table rows and breaks into an array of slides, which contain the calculated amount of table rows that fit on that slide
- * @param {TableCell[][]} tableRows - table rows
- * @param {TableToSlidesProps} tableProps - table2slides properties
- * @param {PresLayout} presLayout - presentation layout
- * @param {SlideLayout} masterSlide - master slide
- * @return {TableRowSlide[]} array of table rows
- */
- export function getSlidesForTableRows(tableRows: TableCell[][] = [], tableProps: TableToSlidesProps = {}, presLayout: PresLayout, masterSlide?: SlideLayout): TableRowSlide[] {
- let arrInchMargins = DEF_SLIDE_MARGIN_IN
- let emuSlideTabW = EMU * 1
- let emuSlideTabH = EMU * 1
- let emuTabCurrH = 0
- let numCols = 0
- const tableRowSlides: TableRowSlide[] = []
- const tablePropX = getSmartParseNumber(tableProps.x, 'X', presLayout)
- const tablePropY = getSmartParseNumber(tableProps.y, 'Y', presLayout)
- const tablePropW = getSmartParseNumber(tableProps.w, 'X', presLayout)
- const tablePropH = getSmartParseNumber(tableProps.h, 'Y', presLayout)
- let tableCalcW = tablePropW
- function calcSlideTabH(): void {
- let emuStartY = 0
- if (tableRowSlides.length === 0) emuStartY = tablePropY || inch2Emu(arrInchMargins[0])
- if (tableRowSlides.length > 0) emuStartY = inch2Emu(tableProps.autoPageSlideStartY || tableProps.newSlideStartY || arrInchMargins[0])
- emuSlideTabH = (tablePropH || presLayout.height) - emuStartY - inch2Emu(arrInchMargins[2])
- // console.log(`| startY .......................................... = ${(emuStartY / EMU).toFixed(1)}`)
- // console.log(`| emuSlideTabH .................................... = ${(emuSlideTabH / EMU).toFixed(1)}`)
- if (tableRowSlides.length > 1) {
- // D: RULE: Use margins for starting point after the initial Slide, not `opt.y` (ISSUE #43, ISSUE #47, ISSUE #48)
- if (typeof tableProps.autoPageSlideStartY === 'number') {
- emuSlideTabH = (tablePropH || presLayout.height) - inch2Emu(tableProps.autoPageSlideStartY + arrInchMargins[2])
- } else if (typeof tableProps.newSlideStartY === 'number') {
- // @deprecated v3.3.0
- emuSlideTabH = (tablePropH || presLayout.height) - inch2Emu(tableProps.newSlideStartY + arrInchMargins[2])
- } else if (tablePropY) {
- emuSlideTabH = (tablePropH || presLayout.height) - inch2Emu((tablePropY / EMU < arrInchMargins[0] ? tablePropY / EMU : arrInchMargins[0]) + arrInchMargins[2])
- // Use whichever is greater: area between margins or the table H provided (dont shrink usable area - the whole point of over-riding Y on paging is to *increase* usable space)
- if (emuSlideTabH < tablePropH) emuSlideTabH = tablePropH
- }
- }
- }
- if (tableProps.verbose) {
- console.log('[[VERBOSE MODE]]')
- console.log('|-- TABLE PROPS --------------------------------------------------------|')
- console.log(`| presLayout.width ................................ = ${(presLayout.width / EMU).toFixed(1)}`)
- console.log(`| presLayout.height ............................... = ${(presLayout.height / EMU).toFixed(1)}`)
- console.log(`| tableProps.x .................................... = ${typeof tableProps.x === 'number' ? (tableProps.x / EMU).toFixed(1) : tableProps.x}`)
- console.log(`| tableProps.y .................................... = ${typeof tableProps.y === 'number' ? (tableProps.y / EMU).toFixed(1) : tableProps.y}`)
- console.log(`| tableProps.w .................................... = ${typeof tableProps.w === 'number' ? (tableProps.w / EMU).toFixed(1) : tableProps.w}`)
- console.log(`| tableProps.h .................................... = ${typeof tableProps.h === 'number' ? (tableProps.h / EMU).toFixed(1) : tableProps.h}`)
- console.log(`| tableProps.slideMargin .......................... = ${tableProps.slideMargin ? String(tableProps.slideMargin) : ''}`)
- console.log(`| tableProps.margin ............................... = ${String(tableProps.margin)}`)
- console.log(`| tableProps.colW ................................. = ${String(tableProps.colW)}`)
- console.log(`| tableProps.autoPageSlideStartY .................. = ${tableProps.autoPageSlideStartY}`)
- console.log(`| tableProps.autoPageCharWeight ................... = ${tableProps.autoPageCharWeight}`)
- console.log('|-- CALCULATIONS -------------------------------------------------------|')
- console.log(`| tablePropX ...................................... = ${tablePropX / EMU}`)
- console.log(`| tablePropY ...................................... = ${tablePropY / EMU}`)
- console.log(`| tablePropW ...................................... = ${tablePropW / EMU}`)
- console.log(`| tablePropH ...................................... = ${tablePropH / EMU}`)
- console.log(`| tableCalcW ...................................... = ${tableCalcW / EMU}`)
- }
- // STEP 1: Calculate margins
- {
- // Important: Use default size as zero cell margin is causing our tables to be too large and touch bottom of slide!
- if (!tableProps.slideMargin && tableProps.slideMargin !== 0) tableProps.slideMargin = DEF_SLIDE_MARGIN_IN[0]
- if (masterSlide && typeof masterSlide._margin !== 'undefined') {
- if (Array.isArray(masterSlide._margin)) arrInchMargins = masterSlide._margin
- else if (!isNaN(Number(masterSlide._margin))) { arrInchMargins = [Number(masterSlide._margin), Number(masterSlide._margin), Number(masterSlide._margin), Number(masterSlide._margin)] }
- } else if (tableProps.slideMargin || tableProps.slideMargin === 0) {
- if (Array.isArray(tableProps.slideMargin)) arrInchMargins = tableProps.slideMargin
- else if (!isNaN(tableProps.slideMargin)) arrInchMargins = [tableProps.slideMargin, tableProps.slideMargin, tableProps.slideMargin, tableProps.slideMargin]
- }
- if (tableProps.verbose) console.log(`| arrInchMargins .................................. = [${arrInchMargins.join(', ')}]`)
- }
- // STEP 2: Calculate number of columns
- {
- // NOTE: Cells may have a colspan, so merely taking the length of the [0] (or any other) row is not
- // ....: sufficient to determine column count. Therefore, check each cell for a colspan and total cols as reqd
- const firstRow = tableRows[0] || []
- firstRow.forEach(cell => {
- if (!cell) cell = { _type: SLIDE_OBJECT_TYPES.tablecell }
- const cellOpts = cell.options || null
- numCols += Number(cellOpts?.colspan ? cellOpts.colspan : 1)
- })
- if (tableProps.verbose) console.log(`| numCols ......................................... = ${numCols}`)
- }
- // STEP 3: Calculate width using tableProps.colW if possible
- if (!tablePropW && tableProps.colW) {
- tableCalcW = Array.isArray(tableProps.colW) ? tableProps.colW.reduce((p, n) => p + n) * EMU : tableProps.colW * numCols || 0
- if (tableProps.verbose) console.log(`| tableCalcW ...................................... = ${tableCalcW / EMU}`)
- }
- // STEP 4: Calculate usable width now that total usable space is known (`emuSlideTabW`)
- {
- emuSlideTabW = tableCalcW || inch2Emu((tablePropX ? tablePropX / EMU : arrInchMargins[1]) + arrInchMargins[3])
- if (tableProps.verbose) console.log(`| emuSlideTabW .................................... = ${(emuSlideTabW / EMU).toFixed(1)}`)
- }
- // STEP 5: Calculate column widths if not provided (emuSlideTabW will be used below to determine lines-per-col)
- if (!tableProps.colW || !Array.isArray(tableProps.colW)) {
- if (tableProps.colW && !isNaN(Number(tableProps.colW))) {
- const arrColW = []
- const firstRow = tableRows[0] || []
- firstRow.forEach(() => arrColW.push(tableProps.colW))
- tableProps.colW = []
- arrColW.forEach(val => {
- if (Array.isArray(tableProps.colW)) tableProps.colW.push(val)
- })
- } else {
- // No column widths provided? Then distribute cols.
- tableProps.colW = []
- for (let iCol = 0; iCol < numCols; iCol++) {
- tableProps.colW.push(emuSlideTabW / EMU / numCols)
- }
- }
- }
- // STEP 6: **MAIN** Iterate over rows, add table content, create new slides as rows overflow
- let newTableRowSlide: TableRowSlide = { rows: [] as TableRow[] }
- tableRows.forEach((row, iRow) => {
- // A: Row variables
- const rowCellLines: TableCell[] = []
- let maxCellMarTopEmu = 0
- let maxCellMarBtmEmu = 0
- // B: Create new row in data model, calc `maxCellMar*`
- let currTableRow: TableRow = []
- row.forEach(cell => {
- currTableRow.push({
- _type: SLIDE_OBJECT_TYPES.tablecell,
- text: [],
- options: cell.options,
- })
- /** FUTURE: DEPRECATED:
- * - Backwards-Compat: Oops! Discovered we were still using points for cell margin before v3.8.0 (UGH!)
- * - We cant introduce a breaking change before v4.0, so...
- */
- if (cell.options.margin && cell.options.margin[0] >= 1) {
- if (cell.options?.margin && cell.options.margin[0] && valToPts(cell.options.margin[0]) > maxCellMarTopEmu) maxCellMarTopEmu = valToPts(cell.options.margin[0])
- else if (tableProps?.margin && tableProps.margin[0] && valToPts(tableProps.margin[0]) > maxCellMarTopEmu) maxCellMarTopEmu = valToPts(tableProps.margin[0])
- if (cell.options?.margin && cell.options.margin[2] && valToPts(cell.options.margin[2]) > maxCellMarBtmEmu) maxCellMarBtmEmu = valToPts(cell.options.margin[2])
- else if (tableProps?.margin && tableProps.margin[2] && valToPts(tableProps.margin[2]) > maxCellMarBtmEmu) maxCellMarBtmEmu = valToPts(tableProps.margin[2])
- } else {
- if (cell.options?.margin && cell.options.margin[0] && inch2Emu(cell.options.margin[0]) > maxCellMarTopEmu) maxCellMarTopEmu = inch2Emu(cell.options.margin[0])
- else if (tableProps?.margin && tableProps.margin[0] && inch2Emu(tableProps.margin[0]) > maxCellMarTopEmu) maxCellMarTopEmu = inch2Emu(tableProps.margin[0])
- if (cell.options?.margin && cell.options.margin[2] && inch2Emu(cell.options.margin[2]) > maxCellMarBtmEmu) maxCellMarBtmEmu = inch2Emu(cell.options.margin[2])
- else if (tableProps?.margin && tableProps.margin[2] && inch2Emu(tableProps.margin[2]) > maxCellMarBtmEmu) maxCellMarBtmEmu = inch2Emu(tableProps.margin[2])
- }
- })
- // C: Calc usable vertical space/table height. Set default value first, adjust below when necessary.
- calcSlideTabH()
- emuTabCurrH += maxCellMarTopEmu + maxCellMarBtmEmu // Start row height with margins
- if (tableProps.verbose && iRow === 0) console.log(`| SLIDE [${tableRowSlides.length}]: emuSlideTabH ...... = ${(emuSlideTabH / EMU).toFixed(1)} `)
- // D: --==[[ BUILD DATA SET ]]==-- (iterate over cells: split text into lines[], set `lineHeight`)
- row.forEach((cell, iCell) => {
- const newCell: TableCell = {
- _type: SLIDE_OBJECT_TYPES.tablecell,
- _lines: null,
- _lineHeight: inch2Emu(
- ((cell.options?.fontSize ? cell.options.fontSize : tableProps.fontSize ? tableProps.fontSize : DEF_FONT_SIZE) *
- (LINEH_MODIFIER + (tableProps.autoPageLineWeight ? tableProps.autoPageLineWeight : 0))) /
- 100
- ),
- text: [],
- options: cell.options,
- }
- // E-1: Exempt cells with `rowspan` from increasing lineHeight (or we could create a new slide when unecessary!)
- if (newCell.options.rowspan) newCell._lineHeight = 0
- // E-2: The parseTextToLines method uses `autoPageCharWeight`, so inherit from table options
- newCell.options.autoPageCharWeight = tableProps.autoPageCharWeight ? tableProps.autoPageCharWeight : null
- // E-3: **MAIN** Parse cell contents into lines based upon col width, font, etc
- let totalColW = tableProps.colW[iCell]
- if (cell.options.colspan && Array.isArray(tableProps.colW)) {
- totalColW = tableProps.colW.filter((_cell, idx) => idx >= iCell && idx < idx + cell.options.colspan).reduce((prev, curr) => prev + curr)
- }
- // E-4: Create lines based upon available column width
- newCell._lines = parseTextToLines(cell, totalColW, false)
- // E-5: Add cell to array
- rowCellLines.push(newCell)
- })
- /** E: --==[[ PAGE DATA SET ]]==--
- * Add text one-line-a-time to this row's cells until: lines are exhausted OR table height limit is hit
- *
- * Design:
- * - Building cells L-to-R/loop style wont work as one could be 100 lines and another 1 line
- * - Therefore, build the whole row, one-line-at-a-time, across each table columns
- * - Then, when the vertical size limit is hit is by any of the cells, make a new slide and continue adding any remaining lines
- *
- * Implementation:
- * - `rowCellLines` is an array of cells, one for each column in the table, with each cell containing an array of lines
- *
- * Sample Data:
- * - `rowCellLines` ..: [ TableCell, TableCell, TableCell ]
- * - `TableCell` .....: { _type: 'tablecell', _lines: TableCell[], _lineHeight: 10 }
- * - `_lines` ........: [ {_type: 'tablecell', text: 'cell-1,line-1', options: {…}}, {_type: 'tablecell', text: 'cell-1,line-2', options: {…}} }
- * - `_lines` is TableCell[] (the 1-N words in the line)
- * {
- * _lines: [{ text:'cell-1,line-1' }, { text:'cell-1,line-2' }], // TOTAL-CELL-HEIGHT = 2
- * _lines: [{ text:'cell-2,line-1' }, { text:'cell-2,line-2' }], // TOTAL-CELL-HEIGHT = 2
- * _lines: [{ text:'cell-3,line-1' }, { text:'cell-3,line-2' }, { text:'cell-3,line-3' }, { text:'cell-3,line-4' }], // TOTAL-CELL-HEIGHT = 4
- * }
- *
- * Example: 2 rows, with the firstrow overflowing onto a new slide
- * SLIDE 1:
- * |--------|--------|--------|--------|
- * | line-1 | line-1 | line-1 | line-1 |
- * | | | line-2 | |
- * | | | line-3 | |
- * |--------|--------|--------|--------|
- *
- * SLIDE 2:
- * |--------|--------|--------|--------|
- * | | | line-4 | |
- * |--------|--------|--------|--------|
- * | line-1 | line-1 | line-1 | line-1 |
- * |--------|--------|--------|--------|
- */
- if (tableProps.verbose) console.log(`\n| SLIDE [${tableRowSlides.length}]: ROW [${iRow}]: START...`)
- let currCellIdx = 0
- let emuLineMaxH = 0
- let isDone = false
- while (!isDone) {
- const srcCell: TableCell = rowCellLines[currCellIdx]
- let tgtCell: TableCell = currTableRow[currCellIdx] // NOTE: may be redefined below (a new row may be created, thus changing this value)
- // 1: calc emuLineMaxH
- rowCellLines.forEach(cell => {
- if (cell._lineHeight >= emuLineMaxH) emuLineMaxH = cell._lineHeight
- })
- // 2: create a new slide if there is insufficient room for the current row
- if (emuTabCurrH + emuLineMaxH > emuSlideTabH) {
- if (tableProps.verbose) {
- console.log('\n|-----------------------------------------------------------------------|')
- // prettier-ignore
- console.log(`|-- NEW SLIDE CREATED (currTabH+currLineH > maxH) => ${(emuTabCurrH / EMU).toFixed(2)} + ${(srcCell._lineHeight / EMU).toFixed(2)} > ${emuSlideTabH / EMU}`)
- console.log('|-----------------------------------------------------------------------|\n\n')
- }
- // A: add current row slide or it will be lost (only if it has rows and text)
- if (currTableRow.length > 0 && currTableRow.map(cell => cell.text.length).reduce((p, n) => p + n) > 0) newTableRowSlide.rows.push(currTableRow)
- // B: add current slide to Slides array
- tableRowSlides.push(newTableRowSlide)
- // C: reset working/curr slide to hold rows as they're created
- const newRows: TableRow[] = []
- newTableRowSlide = { rows: newRows }
- // D: reset working/curr row
- currTableRow = []
- row.forEach(cell => currTableRow.push({ _type: SLIDE_OBJECT_TYPES.tablecell, text: [], options: cell.options }))
- // E: Calc usable vertical space/table height now as we may still be in the same row and code above ("C: Calc usable vertical space/table height.") calc may now be invalid
- calcSlideTabH()
- emuTabCurrH += maxCellMarTopEmu + maxCellMarBtmEmu // Start row height with margins
- if (tableProps.verbose) console.log(`| SLIDE [${tableRowSlides.length}]: emuSlideTabH ...... = ${(emuSlideTabH / EMU).toFixed(1)} `)
- // F: reset current table height for this new Slide
- emuTabCurrH = 0
- // G: handle repeat headers option /or/ Add new empty row to continue current lines into
- if ((tableProps.addHeaderToEach || tableProps.autoPageRepeatHeader) && tableProps._arrObjTabHeadRows) {
- tableProps._arrObjTabHeadRows.forEach(row => {
- const newHeadRow: TableRow = []
- let maxLineHeight = 0
- row.forEach(cell => {
- newHeadRow.push(cell)
- if (cell._lineHeight > maxLineHeight) maxLineHeight = cell._lineHeight
- })
- newTableRowSlide.rows.push(newHeadRow)
- emuTabCurrH += maxLineHeight // TODO: what about margins? dont we need to include cell margin in line height?
- })
- }
- // WIP: NEW: TEST THIS!!
- tgtCell = currTableRow[currCellIdx]
- }
- // 3: set array of words that comprise this line
- const currLine: TableCell[] = srcCell._lines.shift()
- // 4: create new line by adding all words from curr line (or add empty if there are no words to avoid "needs repair" issue triggered when cells have null content)
- if (Array.isArray(tgtCell.text)) {
- if (currLine) tgtCell.text = tgtCell.text.concat(currLine)
- else if (tgtCell.text.length === 0) tgtCell.text = tgtCell.text.concat({ _type: SLIDE_OBJECT_TYPES.tablecell, text: '' })
- // IMPORTANT: ^^^ add empty if there are no words to avoid "needs repair" issue triggered when cells have null content
- }
- // 5: increase table height by the curr line height (if we're on the last column)
- if (currCellIdx === rowCellLines.length - 1) emuTabCurrH += emuLineMaxH
- // 6: advance column/cell index (or circle back to first one to continue adding lines)
- currCellIdx = currCellIdx < rowCellLines.length - 1 ? currCellIdx + 1 : 0
- // 7: WIP: done?
- const brent = rowCellLines.map(cell => cell._lines.length).reduce((prev, next) => prev + next)
- if (brent === 0) isDone = true
- }
- // F: Flush/capture row buffer before it resets at the top of this loop
- if (currTableRow.length > 0) newTableRowSlide.rows.push(currTableRow)
- if (tableProps.verbose) {
- console.log(
- `- SLIDE [${tableRowSlides.length}]: ROW [${iRow}]: ...COMPLETE ...... emuTabCurrH = ${(emuTabCurrH / EMU).toFixed(2)} ( emuSlideTabH = ${(
- emuSlideTabH / EMU
- ).toFixed(2)} )`
- )
- }
- })
- // STEP 7: Flush buffer / add final slide
- tableRowSlides.push(newTableRowSlide)
- if (tableProps.verbose) {
- console.log('\n|================================================|')
- console.log(`| FINAL: tableRowSlides.length = ${tableRowSlides.length}`)
- tableRowSlides.forEach(slide => console.log(slide))
- console.log('|================================================|\n\n')
- }
- // LAST:
- return tableRowSlides
- }
- /**
- * Reproduces an HTML table as a PowerPoint table - including column widths, style, etc. - creates 1 or more slides as needed
- * @param {PptxGenJS} pptx - pptxgenjs instance
- * @param {string} tabEleId - HTMLElementID of the table
- * @param {ITableToSlidesOpts} options - array of options (e.g.: tabsize)
- * @param {SlideLayout} masterSlide - masterSlide
- */
- export function genTableToSlides(pptx: PptxGenJS, tabEleId: string, options: TableToSlidesProps = {}, masterSlide?: SlideLayout): void {
- const opts = options || {}
- opts.slideMargin = opts.slideMargin || opts.slideMargin === 0 ? opts.slideMargin : 0.5
- let emuSlideTabW = opts.w || pptx.presLayout.width
- const arrObjTabHeadRows: [TableCell[]?] = []
- const arrObjTabBodyRows: [TableCell[]?] = []
- const arrObjTabFootRows: [TableCell[]?] = []
- const arrColW: number[] = []
- const arrTabColW: number[] = []
- let arrInchMargins: [number, number, number, number] = [0.5, 0.5, 0.5, 0.5] // TRBL-style
- let intTabW = 0
- // REALITY-CHECK:
- if (!document.getElementById(tabEleId)) throw new Error('tableToSlides: Table ID "' + tabEleId + '" does not exist!')
- // STEP 1: Set margins
- if (masterSlide?._margin) {
- if (Array.isArray(masterSlide._margin)) arrInchMargins = masterSlide._margin
- else if (!isNaN(masterSlide._margin)) arrInchMargins = [masterSlide._margin, masterSlide._margin, masterSlide._margin, masterSlide._margin]
- opts.slideMargin = arrInchMargins
- } else if (opts?.slideMargin) {
- if (Array.isArray(opts.slideMargin)) arrInchMargins = opts.slideMargin
- else if (!isNaN(opts.slideMargin)) arrInchMargins = [opts.slideMargin, opts.slideMargin, opts.slideMargin, opts.slideMargin]
- }
- emuSlideTabW = (opts.w ? inch2Emu(opts.w) : pptx.presLayout.width) - inch2Emu(arrInchMargins[1] + arrInchMargins[3])
- if (opts.verbose) {
- console.log('[[VERBOSE MODE]]')
- console.log('|-- `tableToSlides` ----------------------------------------------------|')
- console.log(`| tableProps.h .................................... = ${opts.h}`)
- console.log(`| tableProps.w .................................... = ${opts.w}`)
- console.log(`| pptx.presLayout.width ........................... = ${(pptx.presLayout.width / EMU).toFixed(1)}`)
- console.log(`| pptx.presLayout.height .......................... = ${(pptx.presLayout.height / EMU).toFixed(1)}`)
- console.log(`| emuSlideTabW .................................... = ${(emuSlideTabW / EMU).toFixed(1)}`)
- }
- // STEP 2: Grab table col widths - just find the first availble row, either thead/tbody/tfoot, others may have colspans, who cares, we only need col widths from 1
- let firstRowCells = document.querySelectorAll(`#${tabEleId} tr:first-child th`)
- if (firstRowCells.length === 0) firstRowCells = document.querySelectorAll(`#${tabEleId} tr:first-child td`)
- firstRowCells.forEach((cellEle: Element) => {
- const cell = cellEle as HTMLTableCellElement
- if (cell.getAttribute('colspan')) {
- // Guesstimate (divide evenly) col widths
- // NOTE: both j$query and vanilla selectors return {0} when table is not visible)
- for (let idxc = 0; idxc < Number(cell.getAttribute('colspan')); idxc++) {
- arrTabColW.push(Math.round(cell.offsetWidth / Number(cell.getAttribute('colspan'))))
- }
- } else {
- arrTabColW.push(cell.offsetWidth)
- }
- })
- arrTabColW.forEach(colW => {
- intTabW += colW
- })
- // STEP 3: Calc/Set column widths by using same column width percent from HTML table
- arrTabColW.forEach((colW, idxW) => {
- const intCalcWidth = Number(((Number(emuSlideTabW) * ((colW / intTabW) * 100)) / 100 / EMU).toFixed(2))
- let intMinWidth = 0
- const colSelectorMin = document.querySelector(`#${tabEleId} thead tr:first-child th:nth-child(${idxW + 1})`)
- if (colSelectorMin) intMinWidth = Number(colSelectorMin.getAttribute('data-pptx-min-width'))
- const intSetWidth = 0
- const colSelectorSet = document.querySelector(`#${tabEleId} thead tr:first-child th:nth-child(${idxW + 1})`)
- if (colSelectorSet) intMinWidth = Number(colSelectorSet.getAttribute('data-pptx-width'))
- arrColW.push(intSetWidth || (intMinWidth > intCalcWidth ? intMinWidth : intCalcWidth))
- })
- if (opts.verbose) {
- console.log(`| arrColW ......................................... = [${arrColW.join(', ')}]`)
- }
- // STEP 4: Iterate over each table element and create data arrays (text and opts)
- // NOTE: We create 3 arrays instead of one so we can loop over body then show header/footer rows on first and last page
- const tableParts = ['thead', 'tbody', 'tfoot']
- tableParts.forEach(part => {
- document.querySelectorAll(`#${tabEleId} ${part} tr`).forEach((row: Element) => {
- const htmlRow = row as HTMLTableRowElement
- const arrObjTabCells: TableCell[] = []
- Array.from(htmlRow.cells).forEach(cell => {
- // A: Get RGB text/bkgd colors
- const arrRGB1 = window.getComputedStyle(cell).getPropertyValue('color').replace(/\s+/gi, '').replace('rgba(', '').replace('rgb(', '').replace(')', '').split(',')
- let arrRGB2 = window
- .getComputedStyle(cell)
- .getPropertyValue('background-color')
- .replace(/\s+/gi, '')
- .replace('rgba(', '')
- .replace('rgb(', '')
- .replace(')', '')
- .split(',')
- if (
- // NOTE: (ISSUE#57): Default for unstyled tables is black bkgd, so use white instead
- window.getComputedStyle(cell).getPropertyValue('background-color') === 'rgba(0, 0, 0, 0)' ||
- window.getComputedStyle(cell).getPropertyValue('transparent')
- ) {
- arrRGB2 = ['255', '255', '255']
- }
- // B: Create option object
- const cellOpts: TableCellProps = {
- align: null,
- bold:
- !!(window.getComputedStyle(cell).getPropertyValue('font-weight') === 'bold' ||
- Number(window.getComputedStyle(cell).getPropertyValue('font-weight')) >= 500),
- border: null,
- color: rgbToHex(Number(arrRGB1[0]), Number(arrRGB1[1]), Number(arrRGB1[2])),
- fill: { color: rgbToHex(Number(arrRGB2[0]), Number(arrRGB2[1]), Number(arrRGB2[2])) },
- fontFace:
- (window.getComputedStyle(cell).getPropertyValue('font-family') || '').split(',')[0].replace(/"/g, '').replace('inherit', '').replace('initial', '') ||
- null,
- fontSize: Number(window.getComputedStyle(cell).getPropertyValue('font-size').replace(/[a-z]/gi, '')),
- margin: null,
- colspan: Number(cell.getAttribute('colspan')) || null,
- rowspan: Number(cell.getAttribute('rowspan')) || null,
- valign: null,
- }
- if (['left', 'center', 'right', 'start', 'end'].includes(window.getComputedStyle(cell).getPropertyValue('text-align'))) {
- const align = window.getComputedStyle(cell).getPropertyValue('text-align').replace('start', 'left').replace('end', 'right')
- cellOpts.align = align === 'center' ? 'center' : align === 'left' ? 'left' : align === 'right' ? 'right' : null
- }
- if (['top', 'middle', 'bottom'].includes(window.getComputedStyle(cell).getPropertyValue('vertical-align'))) {
- const valign = window.getComputedStyle(cell).getPropertyValue('vertical-align')
- cellOpts.valign = valign === 'top' ? 'top' : valign === 'middle' ? 'middle' : valign === 'bottom' ? 'bottom' : null
- }
- // C: Add padding [margin] (if any)
- // NOTE: Margins translate: px->pt 1:1 (e.g.: a 20px padded cell looks the same in PPTX as 20pt Text Inset/Padding)
- if (window.getComputedStyle(cell).getPropertyValue('padding-left')) {
- cellOpts.margin = [0, 0, 0, 0]
- const sidesPad = ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']
- sidesPad.forEach((val, idxs) => {
- cellOpts.margin[idxs] = Math.round(Number(window.getComputedStyle(cell).getPropertyValue(val).replace(/\D/gi, '')))
- })
- }
- // D: Add border (if any)
- if (
- window.getComputedStyle(cell).getPropertyValue('border-top-width') ||
- window.getComputedStyle(cell).getPropertyValue('border-right-width') ||
- window.getComputedStyle(cell).getPropertyValue('border-bottom-width') ||
- window.getComputedStyle(cell).getPropertyValue('border-left-width')
- ) {
- cellOpts.border = [null, null, null, null]
- const sidesBor = ['top', 'right', 'bottom', 'left']
- sidesBor.forEach((val, idxb) => {
- const intBorderW = Math.round(
- Number(
- window
- .getComputedStyle(cell)
- .getPropertyValue('border-' + val + '-width')
- .replace('px', '')
- )
- )
- let arrRGB = []
- arrRGB = window
- .getComputedStyle(cell)
- .getPropertyValue('border-' + val + '-color')
- .replace(/\s+/gi, '')
- .replace('rgba(', '')
- .replace('rgb(', '')
- .replace(')', '')
- .split(',')
- const strBorderC = rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]))
- cellOpts.border[idxb] = { pt: intBorderW, color: strBorderC }
- })
- }
- // LAST: Add cell
- arrObjTabCells.push({
- _type: SLIDE_OBJECT_TYPES.tablecell,
- text: cell.innerText, // `innerText` returns <br> as "\n", so linebreak etc. work later!
- options: cellOpts,
- })
- })
- switch (part) {
- case 'thead':
- arrObjTabHeadRows.push(arrObjTabCells)
- break
- case 'tbody':
- arrObjTabBodyRows.push(arrObjTabCells)
- break
- case 'tfoot':
- arrObjTabFootRows.push(arrObjTabCells)
- break
- default:
- console.log(`table parsing: unexpected table part: ${part}`)
- break
- }
- })
- })
- // STEP 5: Break table into Slides as needed
- // Pass head-rows as there is an option to add to each table and the parse func needs this data to fulfill that option
- opts._arrObjTabHeadRows = arrObjTabHeadRows || null
- opts.colW = arrColW
- getSlidesForTableRows([...arrObjTabHeadRows, ...arrObjTabBodyRows, ...arrObjTabFootRows], opts, pptx.presLayout, masterSlide).forEach((slide, idxTr) => {
- // A: Create new Slide
- const newSlide = pptx.addSlide({ masterName: opts.masterSlideName || null })
- // B: DESIGN: Reset `y` to startY or margin after first Slide (ISSUE#43, ISSUE#47, ISSUE#48)
- if (idxTr === 0) opts.y = opts.y || arrInchMargins[0]
- if (idxTr > 0) opts.y = opts.autoPageSlideStartY || opts.newSlideStartY || arrInchMargins[0]
- if (opts.verbose) console.log(`| opts.autoPageSlideStartY: ${opts.autoPageSlideStartY} / arrInchMargins[0]: ${arrInchMargins[0]} => opts.y = ${opts.y}`)
- // C: Add table to Slide
- newSlide.addTable(slide.rows, { x: opts.x || arrInchMargins[3], y: opts.y, w: Number(emuSlideTabW) / EMU, colW: arrColW, autoPage: false })
- // D: Add any additional objects
- if (opts.addImage) {
- opts.addImage.options = opts.addImage.options || {}
- if (!opts.addImage.image || (!opts.addImage.image.path && !opts.addImage.image.data)) {
- console.warn('Warning: tableToSlides.addImage requires either `path` or `data`')
- } else {
- newSlide.addImage({
- path: opts.addImage.image.path,
- data: opts.addImage.image.data,
- x: opts.addImage.options.x,
- y: opts.addImage.options.y,
- w: opts.addImage.options.w,
- h: opts.addImage.options.h,
- })
- }
- }
- if (opts.addShape) newSlide.addShape(opts.addShape.shapeName, opts.addShape.options || {})
- if (opts.addTable) newSlide.addTable(opts.addTable.rows, opts.addTable.options || {})
- if (opts.addText) newSlide.addText(opts.addText.text, opts.addText.options || {})
- })
- }
|