node-pptx-templater 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +155 -2
- package/src/managers/TableManager.js +712 -115
package/README.md
CHANGED
|
@@ -173,6 +173,22 @@ if (!report.valid) {
|
|
|
173
173
|
}
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### 5. PPTX Extraction & Rebuilding Utilities
|
|
177
|
+
|
|
178
|
+
You can programmatically extract standard zipped `.pptx` archives into OpenXML folder templates or compile them back. Target directories are created automatically:
|
|
179
|
+
|
|
180
|
+
#### Extract PPTX to Folder Template
|
|
181
|
+
```javascript
|
|
182
|
+
const { PPTXTemplate } = require('node-pptx-templater');
|
|
183
|
+
|
|
184
|
+
await PPTXTemplate.extractPptx('./sample.pptx', './output/template', { overwrite: true });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Rebuild PPTX from Folder Template
|
|
188
|
+
```javascript
|
|
189
|
+
await PPTXTemplate.buildPptx('./templates/sample', './output.pptx');
|
|
190
|
+
```
|
|
191
|
+
|
|
176
192
|
---
|
|
177
193
|
|
|
178
194
|
## 📋 OpenXML Presentation Architecture
|
|
@@ -208,6 +224,95 @@ Naively duplicating rows in slide tables can leave duplicate `rowId` values or b
|
|
|
208
224
|
|
|
209
225
|
---
|
|
210
226
|
|
|
227
|
+
## 📊 Reading Table Data & JSON Extraction
|
|
228
|
+
|
|
229
|
+
PPTXForge allows you to read table data from your template and return it as structured JSON objects or raw arrays. This is extremely useful for reverse-engineering data or verifying layouts.
|
|
230
|
+
|
|
231
|
+
### 1. Object-Based Extraction (Default)
|
|
232
|
+
By default, the first row is treated as headers, and subsequent rows are returned as objects keyed by header names. Merged cells automatically resolve to their parent cell's value:
|
|
233
|
+
```javascript
|
|
234
|
+
const rows = await ppt.getTableRows('SalesTable');
|
|
235
|
+
// Returns:
|
|
236
|
+
// [
|
|
237
|
+
// { region: 'North', sales: '1200', growth: '15%' },
|
|
238
|
+
// { region: 'South', sales: '1800', growth: '22%' }
|
|
239
|
+
// ]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 2. Raw Extraction
|
|
243
|
+
Return a raw 2D array of string values (excluding the header row) by passing `{ raw: true }`:
|
|
244
|
+
```javascript
|
|
245
|
+
const rows = await ppt.getTableRows('SalesTable', { raw: true });
|
|
246
|
+
// Returns:
|
|
247
|
+
// [
|
|
248
|
+
// ['North', '1200', '15%'],
|
|
249
|
+
// ['South', '1800', '22%']
|
|
250
|
+
// ]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 3. Including Table Metadata
|
|
254
|
+
Pass `{ includeMetadata: true }` to retrieve row counts, column counts, and the list of merged cell ranges alongside the rows:
|
|
255
|
+
```javascript
|
|
256
|
+
const result = await ppt.getTableRows('SalesTable', { includeMetadata: true });
|
|
257
|
+
// Returns:
|
|
258
|
+
// {
|
|
259
|
+
// rows: [...],
|
|
260
|
+
// rowCount: 10,
|
|
261
|
+
// columnCount: 5,
|
|
262
|
+
// mergedCells: [ { startRow: 1, startCol: 0, endRow: 2, endCol: 0 }, ... ]
|
|
263
|
+
// }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 📈 Nested Table Rows & Rowspan Support
|
|
269
|
+
|
|
270
|
+
When building financial sheets, invoices, or hierarchical dashboards, you often need to stack multiple rows vertically inside a single column while spanning cells in other columns. PPTXForge supports nested arrays in `addTableRow()` to automatically handle:
|
|
271
|
+
- Proportional height scaling.
|
|
272
|
+
- Vertical merge (`vMerge`) and row span (`rowSpan`) generation.
|
|
273
|
+
- Dynamic layout adjustments.
|
|
274
|
+
|
|
275
|
+
### 1. Vertical Row Nesting Example
|
|
276
|
+
```javascript
|
|
277
|
+
await ppt.addTableRow('Table1', [
|
|
278
|
+
'Region',
|
|
279
|
+
['Sales', '1200'],
|
|
280
|
+
['Growth', '15%']
|
|
281
|
+
]);
|
|
282
|
+
```
|
|
283
|
+
Generates:
|
|
284
|
+
```text
|
|
285
|
+
+---------+---------+---------+
|
|
286
|
+
| Region | Sales | Growth |
|
|
287
|
+
| | 1200 | 15% |
|
|
288
|
+
+---------+---------+---------+
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 2. Deep Nesting Example
|
|
292
|
+
```javascript
|
|
293
|
+
await ppt.addTableRow('Table1', [
|
|
294
|
+
'Parent',
|
|
295
|
+
[
|
|
296
|
+
'Child 1',
|
|
297
|
+
'Child 2',
|
|
298
|
+
'Child 3'
|
|
299
|
+
]
|
|
300
|
+
]);
|
|
301
|
+
```
|
|
302
|
+
Generates 3 sub-rows where column 0 automatically spans all 3 rows.
|
|
303
|
+
|
|
304
|
+
### 3. Merge Strategies
|
|
305
|
+
Configure merge behavior via `options.mergeStrategy`:
|
|
306
|
+
- `'auto'` (default): Creates structural merges from nested arrays, and additionally merges consecutive duplicate values in the same column.
|
|
307
|
+
- `'rowspan'`: Creates structural rowspan/merges strictly from the nested array structure.
|
|
308
|
+
- `'none'`: Pads columns to match the target generated height but does not merge cells (leaving them as individual cells).
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
await ppt.addTableRow('Table1', data, { mergeStrategy: 'rowspan' });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
211
316
|
## 📊 Feature Comparison Matrix
|
|
212
317
|
|
|
213
318
|
Compare PPTXForge with other popular PowerPoint automation libraries:
|
|
@@ -300,6 +405,41 @@ Discovers and retrieves details of an existing cell shape on the targeted slide.
|
|
|
300
405
|
const shape = ppt.getCellShape('Table', 1, 2, 0);
|
|
301
406
|
```
|
|
302
407
|
|
|
408
|
+
#### `getCellBounds(tableId, rowIndex, colIndex)`
|
|
409
|
+
Retrieves final rendered bounds of a table cell in pixels.
|
|
410
|
+
|
|
411
|
+
* **Arguments**:
|
|
412
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
413
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
414
|
+
* `colIndex` (`number`): 0-based column index.
|
|
415
|
+
* **Returns**: `Object|null` - Cell bounds { x, y, width, height } in pixels, or null.
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
const bounds = ppt.getCellBounds('summary-table', 1, 1);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### `getCellPosition(tableId, rowIndex, colIndex)`
|
|
422
|
+
Retrieves final rendered position of a table cell in pixels.
|
|
423
|
+
|
|
424
|
+
* **Arguments**:
|
|
425
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
426
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
427
|
+
* `colIndex` (`number`): 0-based column index.
|
|
428
|
+
* **Returns**: `Object|null` - Cell position { row, column, x, y } in pixels, or null.
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
const pos = ppt.getCellPosition('summary-table', 1, 1);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### `getTableRows(())`
|
|
435
|
+
Delegates core actions to slide element sub-managers.
|
|
436
|
+
|
|
437
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
const rows = await ppt.getTableRows('SalesTable');
|
|
441
|
+
```
|
|
442
|
+
|
|
303
443
|
#### `addTableRow(())`
|
|
304
444
|
Delegates core actions to slide element sub-managers.
|
|
305
445
|
|
|
@@ -1587,6 +1727,24 @@ Delegates core actions to slide element sub-managers.
|
|
|
1587
1727
|
const ppt = await PPTXTemplate.fromPresentationXml('./template-folder');
|
|
1588
1728
|
```
|
|
1589
1729
|
|
|
1730
|
+
#### `extractPptx(())`
|
|
1731
|
+
Delegates core actions to slide element sub-managers.
|
|
1732
|
+
|
|
1733
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
1734
|
+
|
|
1735
|
+
```javascript
|
|
1736
|
+
await PPTXTemplater.extractPptx('sample.pptx', './extracted');
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
#### `buildPptx(())`
|
|
1740
|
+
Delegates core actions to slide element sub-managers.
|
|
1741
|
+
|
|
1742
|
+
* **Returns**: `PPTXTemplater` - The fluent engine instance.
|
|
1743
|
+
|
|
1744
|
+
```javascript
|
|
1745
|
+
await PPTXTemplater.buildPptx('./extracted', 'output.pptx');
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1590
1748
|
#### `validatePresentation(())`
|
|
1591
1749
|
Delegates core actions to slide element sub-managers.
|
|
1592
1750
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -340,6 +340,89 @@ class PPTXTemplater {
|
|
|
340
340
|
return engine
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Extracts a PPTX file into an unzipped OpenXML folder structure.
|
|
345
|
+
*
|
|
346
|
+
* @static
|
|
347
|
+
* @param {string} pptxPath - Path to the source PPTX file.
|
|
348
|
+
* @param {string} outputPath - Path to the destination folder.
|
|
349
|
+
* @param {Object} [options] - Options (e.g. { overwrite: true }).
|
|
350
|
+
* @returns {Promise<void>}
|
|
351
|
+
*/
|
|
352
|
+
static async extractPptx(pptxPath, outputPath, options = {}) {
|
|
353
|
+
const fs = require('fs-extra')
|
|
354
|
+
const path = require('path')
|
|
355
|
+
|
|
356
|
+
const resolvedPptx = path.resolve(pptxPath)
|
|
357
|
+
const resolvedOut = path.resolve(outputPath)
|
|
358
|
+
|
|
359
|
+
if (!fs.existsSync(resolvedPptx)) {
|
|
360
|
+
throw new PPTXError(`Source PPTX file not found: ${pptxPath}`)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (fs.existsSync(resolvedOut)) {
|
|
364
|
+
const stats = fs.statSync(resolvedOut)
|
|
365
|
+
if (stats.isFile()) {
|
|
366
|
+
throw new PPTXError(`Destination is a file: ${outputPath}`)
|
|
367
|
+
}
|
|
368
|
+
const files = fs.readdirSync(resolvedOut)
|
|
369
|
+
if (files.length > 0 && !options.overwrite) {
|
|
370
|
+
throw new PPTXError(
|
|
371
|
+
`Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
await fs.ensureDir(resolvedOut)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const engine = await PPTXTemplater.load(resolvedPptx)
|
|
379
|
+
await engine.#zipManager.toFolder(resolvedOut)
|
|
380
|
+
|
|
381
|
+
// Validation
|
|
382
|
+
const criticalParts = ['ppt/presentation.xml', 'ppt/slides', 'ppt/_rels', '[Content_Types].xml']
|
|
383
|
+
|
|
384
|
+
for (const part of criticalParts) {
|
|
385
|
+
const p = path.join(resolvedOut, part)
|
|
386
|
+
if (!fs.existsSync(p)) {
|
|
387
|
+
throw new PPTXError(`Extracted structure is missing critical part: ${part}`)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Rebuilds a PPTX file from an unzipped OpenXML folder structure.
|
|
394
|
+
*
|
|
395
|
+
* @static
|
|
396
|
+
* @param {string} folderPath - Path to the source folder structure.
|
|
397
|
+
* @param {string} pptxPath - Path to the destination PPTX file.
|
|
398
|
+
* @returns {Promise<void>}
|
|
399
|
+
*/
|
|
400
|
+
static async buildPptx(folderPath, pptxPath) {
|
|
401
|
+
const fs = require('fs-extra')
|
|
402
|
+
const path = require('path')
|
|
403
|
+
|
|
404
|
+
const resolvedFolder = path.resolve(folderPath)
|
|
405
|
+
const resolvedPptx = path.resolve(pptxPath)
|
|
406
|
+
|
|
407
|
+
if (!fs.existsSync(resolvedFolder)) {
|
|
408
|
+
throw new PPTXError(`Source folder not found: ${folderPath}`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Validation of the source folder
|
|
412
|
+
const criticalParts = ['ppt/presentation.xml', 'ppt/slides', 'ppt/_rels', '[Content_Types].xml']
|
|
413
|
+
|
|
414
|
+
for (const part of criticalParts) {
|
|
415
|
+
const p = path.join(resolvedFolder, part)
|
|
416
|
+
if (!fs.existsSync(p)) {
|
|
417
|
+
throw new PPTXError(`Source folder is missing critical OpenXML part: ${part}`)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const engine = await PPTXTemplater.load(resolvedFolder)
|
|
422
|
+
await fs.ensureDir(path.dirname(resolvedPptx))
|
|
423
|
+
await engine.saveToFile(resolvedPptx)
|
|
424
|
+
}
|
|
425
|
+
|
|
343
426
|
/**
|
|
344
427
|
* Initializes the engine by loading a PPTX file/buffer.
|
|
345
428
|
* @private
|
|
@@ -1297,11 +1380,21 @@ class PPTXTemplater {
|
|
|
1297
1380
|
}
|
|
1298
1381
|
|
|
1299
1382
|
// === Table Features ===
|
|
1300
|
-
|
|
1383
|
+
getTableRows(tableId, options = {}) {
|
|
1384
|
+
this.#assertLoaded()
|
|
1385
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1386
|
+
if (targetIndices.length === 0) {
|
|
1387
|
+
throw new PPTXError('No slides active/loaded')
|
|
1388
|
+
}
|
|
1389
|
+
const idx = targetIndices[0]
|
|
1390
|
+
return this.#tableManager.getTableRows(idx, tableId, options, this.#slideManager)
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
addTableRow(tableId, rowData, options = {}) {
|
|
1301
1394
|
this.#assertLoaded()
|
|
1302
1395
|
const targetIndices = this.#getTargetSlideIndices()
|
|
1303
1396
|
for (const idx of targetIndices) {
|
|
1304
|
-
this.#tableManager.addTableRow(idx, tableId, rowData, this.#slideManager)
|
|
1397
|
+
this.#tableManager.addTableRow(idx, tableId, rowData, this.#slideManager, options)
|
|
1305
1398
|
}
|
|
1306
1399
|
return this
|
|
1307
1400
|
}
|
|
@@ -2099,6 +2192,66 @@ class PPTXTemplater {
|
|
|
2099
2192
|
return null
|
|
2100
2193
|
}
|
|
2101
2194
|
|
|
2195
|
+
/**
|
|
2196
|
+
* Retrieves final rendered bounds of a table cell in pixels.
|
|
2197
|
+
*
|
|
2198
|
+
* @param {string} tableId - Table name or shape ID.
|
|
2199
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2200
|
+
* @param {number} colIndex - 0-based column index.
|
|
2201
|
+
* @returns {Object|null} Cell bounds { x, y, width, height } in pixels, or null.
|
|
2202
|
+
*/
|
|
2203
|
+
getCellBounds(tableId, rowIndex, colIndex) {
|
|
2204
|
+
this.#assertLoaded()
|
|
2205
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2206
|
+
for (const idx of targetIndices) {
|
|
2207
|
+
try {
|
|
2208
|
+
const bounds = this.#tableManager.getCellBounds(
|
|
2209
|
+
idx,
|
|
2210
|
+
tableId,
|
|
2211
|
+
rowIndex,
|
|
2212
|
+
colIndex,
|
|
2213
|
+
this.#slideManager
|
|
2214
|
+
)
|
|
2215
|
+
if (bounds) return bounds
|
|
2216
|
+
} catch (err) {
|
|
2217
|
+
logger.debug(
|
|
2218
|
+
`Could not get cell bounds for table ${tableId} on slide ${idx}: ${err.message}`
|
|
2219
|
+
)
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return null
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Retrieves final rendered position of a table cell in pixels.
|
|
2227
|
+
*
|
|
2228
|
+
* @param {string} tableId - Table name or shape ID.
|
|
2229
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2230
|
+
* @param {number} colIndex - 0-based column index.
|
|
2231
|
+
* @returns {Object|null} Cell position { row, column, x, y } in pixels, or null.
|
|
2232
|
+
*/
|
|
2233
|
+
getCellPosition(tableId, rowIndex, colIndex) {
|
|
2234
|
+
this.#assertLoaded()
|
|
2235
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2236
|
+
for (const idx of targetIndices) {
|
|
2237
|
+
try {
|
|
2238
|
+
const pos = this.#tableManager.getCellPosition(
|
|
2239
|
+
idx,
|
|
2240
|
+
tableId,
|
|
2241
|
+
rowIndex,
|
|
2242
|
+
colIndex,
|
|
2243
|
+
this.#slideManager
|
|
2244
|
+
)
|
|
2245
|
+
if (pos) return pos
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
logger.debug(
|
|
2248
|
+
`Could not get cell position for table ${tableId} on slide ${idx}: ${err.message}`
|
|
2249
|
+
)
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return null
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2102
2255
|
// === Image Features ===
|
|
2103
2256
|
async replaceImage(imageIdOrName, sourcePathOrBuffer) {
|
|
2104
2257
|
this.#assertLoaded()
|
|
@@ -223,6 +223,8 @@ class TableManager {
|
|
|
223
223
|
)
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
|
|
227
|
+
|
|
226
228
|
if (cellShapes) {
|
|
227
229
|
this.#processCellShapes(
|
|
228
230
|
slideIndex,
|
|
@@ -251,7 +253,7 @@ class TableManager {
|
|
|
251
253
|
* @param {string[]} rowData
|
|
252
254
|
* @param {SlideManager} slideManager
|
|
253
255
|
*/
|
|
254
|
-
addTableRow(slideIndex, tableId, rowData, slideManager) {
|
|
256
|
+
addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
|
|
255
257
|
const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
256
258
|
|
|
257
259
|
const trs = tblObj['a:tr'] || []
|
|
@@ -260,18 +262,69 @@ class TableManager {
|
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
const lastRow = trs[trs.length - 1]
|
|
263
|
-
const
|
|
264
|
-
this.#updateRowId(newRow)
|
|
265
|
+
const numCols = lastRow['a:tc']?.length || 0
|
|
265
266
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
|
|
271
|
-
if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
|
|
267
|
+
// Compute target generated height
|
|
268
|
+
const heights = []
|
|
269
|
+
for (let c = 0; c < numCols; c++) {
|
|
270
|
+
heights.push(this.#getNestedHeight(rowData[c]))
|
|
272
271
|
}
|
|
272
|
+
const targetHeight = Math.max(1, ...heights)
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
// Expand each column value to targetHeight
|
|
275
|
+
const expandedCols = []
|
|
276
|
+
const strategy = options.mergeStrategy || 'auto'
|
|
277
|
+
for (let c = 0; c < numCols; c++) {
|
|
278
|
+
let colCells = this.#expandCellVal(rowData[c], targetHeight)
|
|
279
|
+
if (strategy === 'none') {
|
|
280
|
+
for (let i = 0; i < colCells.length; i++) {
|
|
281
|
+
if (colCells[i].vMerge) {
|
|
282
|
+
colCells[i] = { value: '', rowSpan: 1 }
|
|
283
|
+
} else {
|
|
284
|
+
colCells[i].rowSpan = 1
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else if (strategy === 'auto') {
|
|
288
|
+
colCells = this.#applyAutoMerge(colCells)
|
|
289
|
+
}
|
|
290
|
+
expandedCols.push(colCells)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Clone and append rows
|
|
294
|
+
for (let r = 0; r < targetHeight; r++) {
|
|
295
|
+
const newRow = this.#xmlParser.deepClone(lastRow)
|
|
296
|
+
this.#updateRowId(newRow)
|
|
297
|
+
|
|
298
|
+
const tcs = newRow['a:tc'] || []
|
|
299
|
+
for (let c = 0; c < numCols; c++) {
|
|
300
|
+
const cellDef = expandedCols[c][r]
|
|
301
|
+
const tcObj = tcs[c]
|
|
302
|
+
|
|
303
|
+
// Clear any previous merge attributes
|
|
304
|
+
if (tcObj['@_hMerge']) delete tcObj['@_hMerge']
|
|
305
|
+
if (tcObj['@_vMerge']) delete tcObj['@_vMerge']
|
|
306
|
+
if (tcObj['@_gridSpan']) delete tcObj['@_gridSpan']
|
|
307
|
+
if (tcObj['@_rowSpan']) delete tcObj['@_rowSpan']
|
|
308
|
+
|
|
309
|
+
if (cellDef.vMerge) {
|
|
310
|
+
tcObj['@_vMerge'] = '1'
|
|
311
|
+
this.#setCellTextObj(tcObj, '')
|
|
312
|
+
} else {
|
|
313
|
+
let text = cellDef.value
|
|
314
|
+
let cellOpts = {}
|
|
315
|
+
if (cellDef.value && typeof cellDef.value === 'object') {
|
|
316
|
+
text = cellDef.value.value !== undefined ? cellDef.value.value : ''
|
|
317
|
+
cellOpts = cellDef.value
|
|
318
|
+
}
|
|
319
|
+
this.#setCellTextObj(tcObj, text)
|
|
320
|
+
if (cellDef.rowSpan && cellDef.rowSpan > 1 && strategy !== 'none') {
|
|
321
|
+
tcObj['@_rowSpan'] = String(cellDef.rowSpan)
|
|
322
|
+
}
|
|
323
|
+
this.#applyCellOptions(tcObj, cellOpts)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
trs.push(newRow)
|
|
327
|
+
}
|
|
275
328
|
|
|
276
329
|
slideManager.markSlideObjDirty(slideIndex)
|
|
277
330
|
}
|
|
@@ -740,6 +793,68 @@ class TableManager {
|
|
|
740
793
|
return { row, col }
|
|
741
794
|
}
|
|
742
795
|
|
|
796
|
+
getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
|
|
797
|
+
const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
798
|
+
|
|
799
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
|
|
800
|
+
|
|
801
|
+
const xfrm = frameObj['p:xfrm']
|
|
802
|
+
const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
|
|
803
|
+
const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
|
|
804
|
+
|
|
805
|
+
const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
|
|
806
|
+
const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
|
|
807
|
+
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
808
|
+
|
|
809
|
+
const trsArr = tblObj['a:tr'] || []
|
|
810
|
+
const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
|
|
811
|
+
|
|
812
|
+
const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
813
|
+
const pr = parent.row
|
|
814
|
+
const pc = parent.col
|
|
815
|
+
|
|
816
|
+
let cellLeft = tableX
|
|
817
|
+
for (let idx = 0; idx < pc; idx++) {
|
|
818
|
+
cellLeft += colWidths[idx] || 0
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let cellTop = tableY
|
|
822
|
+
for (let idx = 0; idx < pr; idx++) {
|
|
823
|
+
cellTop += rowHeights[idx] || 0
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const parentCell = trsArr[pr]?.['a:tc']?.[pc]
|
|
827
|
+
const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
|
|
828
|
+
const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
|
|
829
|
+
|
|
830
|
+
let cellWidth = 0
|
|
831
|
+
for (let idx = 0; idx < gridSpan; idx++) {
|
|
832
|
+
cellWidth += colWidths[pc + idx] || 0
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let cellHeight = 0
|
|
836
|
+
for (let idx = 0; idx < rowSpan; idx++) {
|
|
837
|
+
cellHeight += rowHeights[pr + idx] || 0
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
x: Math.round(cellLeft / 9525),
|
|
842
|
+
y: Math.round(cellTop / 9525),
|
|
843
|
+
width: Math.round(cellWidth / 9525),
|
|
844
|
+
height: Math.round(cellHeight / 9525),
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
getCellPosition(slideIndex, tableId, rowIndex, colIndex, slideManager) {
|
|
849
|
+
const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
850
|
+
return {
|
|
851
|
+
row: rowIndex,
|
|
852
|
+
column: colIndex,
|
|
853
|
+
x: bounds.x,
|
|
854
|
+
y: bounds.y,
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
743
858
|
/**
|
|
744
859
|
* Splits a merged region containing cell (row, col).
|
|
745
860
|
*/
|
|
@@ -1152,45 +1267,226 @@ class TableManager {
|
|
|
1152
1267
|
}
|
|
1153
1268
|
|
|
1154
1269
|
#expandCellShape(config, cellBounds) {
|
|
1155
|
-
const cellLeft_px = cellBounds.left / 9525
|
|
1156
|
-
const cellTop_px = cellBounds.top / 9525
|
|
1157
|
-
const cellWidth_px = cellBounds.width / 9525
|
|
1158
|
-
const cellHeight_px = cellBounds.height / 9525
|
|
1270
|
+
const cellLeft_px = Math.round(cellBounds.left / 9525)
|
|
1271
|
+
const cellTop_px = Math.round(cellBounds.top / 9525)
|
|
1272
|
+
const cellWidth_px = Math.round(cellBounds.width / 9525)
|
|
1273
|
+
const cellHeight_px = Math.round(cellBounds.height / 9525)
|
|
1274
|
+
|
|
1275
|
+
const parseLength = (val, maxVal) => {
|
|
1276
|
+
if (typeof val === 'string' && val.endsWith('%')) {
|
|
1277
|
+
return (parseFloat(val) / 100) * maxVal
|
|
1278
|
+
}
|
|
1279
|
+
return val !== undefined ? parseFloat(val) : undefined
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const isCellAnchored = config.anchor !== 'slide'
|
|
1283
|
+
|
|
1284
|
+
// 1. Determine bounding box width and height
|
|
1285
|
+
let shapeWidth
|
|
1286
|
+
let shapeHeight
|
|
1159
1287
|
|
|
1288
|
+
if (config.type === 'progressBar') {
|
|
1289
|
+
shapeHeight = parseLength(config.height !== undefined ? config.height : 8, cellHeight_px)
|
|
1290
|
+
shapeWidth = parseLength(
|
|
1291
|
+
config.width !== undefined ? config.width : cellWidth_px - 10,
|
|
1292
|
+
cellWidth_px
|
|
1293
|
+
)
|
|
1294
|
+
} else if (config.type === 'badge') {
|
|
1295
|
+
const text = String(config.text !== undefined ? config.text : '')
|
|
1296
|
+
const fontSize = config.textStyle?.fontSize || 10
|
|
1297
|
+
const textWidth = text.length * fontSize * 0.6
|
|
1298
|
+
const paddingX = 12
|
|
1299
|
+
shapeWidth =
|
|
1300
|
+
parseLength(config.width, cellWidth_px) !== undefined
|
|
1301
|
+
? parseLength(config.width, cellWidth_px)
|
|
1302
|
+
: textWidth + paddingX * 2
|
|
1303
|
+
shapeHeight =
|
|
1304
|
+
parseLength(config.height, cellHeight_px) !== undefined
|
|
1305
|
+
? parseLength(config.height, cellHeight_px)
|
|
1306
|
+
: fontSize + 12
|
|
1307
|
+
} else if (config.type === 'icon') {
|
|
1308
|
+
const size = parseLength(
|
|
1309
|
+
config.size !== undefined ? config.size : 16,
|
|
1310
|
+
Math.min(cellWidth_px, cellHeight_px)
|
|
1311
|
+
)
|
|
1312
|
+
shapeWidth = size
|
|
1313
|
+
shapeHeight = size
|
|
1314
|
+
} else {
|
|
1315
|
+
shapeWidth = parseLength(config.width, cellWidth_px)
|
|
1316
|
+
if (shapeWidth === undefined) {
|
|
1317
|
+
const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
|
|
1318
|
+
if (sizeVal !== undefined) {
|
|
1319
|
+
shapeWidth = sizeVal
|
|
1320
|
+
} else {
|
|
1321
|
+
const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
|
|
1322
|
+
if (radiusVal !== undefined) {
|
|
1323
|
+
shapeWidth = radiusVal * 2
|
|
1324
|
+
} else {
|
|
1325
|
+
shapeWidth = 12 // default
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
shapeHeight = parseLength(config.height, cellHeight_px)
|
|
1331
|
+
if (shapeHeight === undefined) {
|
|
1332
|
+
const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
|
|
1333
|
+
if (sizeVal !== undefined) {
|
|
1334
|
+
shapeHeight = sizeVal
|
|
1335
|
+
} else {
|
|
1336
|
+
const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
|
|
1337
|
+
if (radiusVal !== undefined) {
|
|
1338
|
+
shapeHeight = radiusVal * 2
|
|
1339
|
+
} else {
|
|
1340
|
+
shapeHeight = 12 // default
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// 2. Determine alignment settings
|
|
1347
|
+
let alignX = config.alignX
|
|
1348
|
+
let alignY = config.alignY
|
|
1349
|
+
|
|
1350
|
+
if (config.position) {
|
|
1351
|
+
switch (config.position) {
|
|
1352
|
+
case 'top-left':
|
|
1353
|
+
if (!alignX) alignX = 'left'
|
|
1354
|
+
if (!alignY) alignY = 'top'
|
|
1355
|
+
break
|
|
1356
|
+
case 'top-center':
|
|
1357
|
+
case 'top':
|
|
1358
|
+
if (!alignX) alignX = 'center'
|
|
1359
|
+
if (!alignY) alignY = 'top'
|
|
1360
|
+
break
|
|
1361
|
+
case 'top-right':
|
|
1362
|
+
if (!alignX) alignX = 'right'
|
|
1363
|
+
if (!alignY) alignY = 'top'
|
|
1364
|
+
break
|
|
1365
|
+
case 'middle-left':
|
|
1366
|
+
case 'left':
|
|
1367
|
+
if (!alignX) alignX = 'left'
|
|
1368
|
+
if (!alignY) alignY = 'middle'
|
|
1369
|
+
break
|
|
1370
|
+
case 'center':
|
|
1371
|
+
case 'middle-center':
|
|
1372
|
+
if (!alignX) alignX = 'center'
|
|
1373
|
+
if (!alignY) alignY = 'middle'
|
|
1374
|
+
break
|
|
1375
|
+
case 'middle-right':
|
|
1376
|
+
case 'right':
|
|
1377
|
+
if (!alignX) alignX = 'right'
|
|
1378
|
+
if (!alignY) alignY = 'middle'
|
|
1379
|
+
break
|
|
1380
|
+
case 'bottom-left':
|
|
1381
|
+
if (!alignX) alignX = 'left'
|
|
1382
|
+
if (!alignY) alignY = 'bottom'
|
|
1383
|
+
break
|
|
1384
|
+
case 'bottom-center':
|
|
1385
|
+
case 'bottom':
|
|
1386
|
+
if (!alignX) alignX = 'center'
|
|
1387
|
+
if (!alignY) alignY = 'bottom'
|
|
1388
|
+
break
|
|
1389
|
+
case 'bottom-right':
|
|
1390
|
+
if (!alignX) alignX = 'right'
|
|
1391
|
+
if (!alignY) alignY = 'bottom'
|
|
1392
|
+
break
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (alignX && !alignY && config.y === undefined) {
|
|
1397
|
+
alignY = 'middle'
|
|
1398
|
+
}
|
|
1399
|
+
if (alignY && !alignX && config.x === undefined) {
|
|
1400
|
+
alignX = 'center'
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
|
|
1404
|
+
alignX = 'center'
|
|
1405
|
+
alignY = 'middle'
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// 3. Compute coordinates
|
|
1409
|
+
let shapeLeft = cellLeft_px
|
|
1410
|
+
let shapeTop = cellTop_px
|
|
1411
|
+
|
|
1412
|
+
if (isCellAnchored) {
|
|
1413
|
+
if (alignX === 'left') {
|
|
1414
|
+
shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
|
|
1415
|
+
} else if (alignX === 'center') {
|
|
1416
|
+
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
|
|
1417
|
+
} else if (alignX === 'right') {
|
|
1418
|
+
shapeLeft =
|
|
1419
|
+
cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
|
|
1420
|
+
} else {
|
|
1421
|
+
shapeLeft =
|
|
1422
|
+
cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (alignY === 'top') {
|
|
1426
|
+
shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
|
|
1427
|
+
} else if (alignY === 'middle') {
|
|
1428
|
+
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
|
|
1429
|
+
} else if (alignY === 'bottom') {
|
|
1430
|
+
shapeTop =
|
|
1431
|
+
cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
|
|
1432
|
+
} else {
|
|
1433
|
+
shapeTop =
|
|
1434
|
+
cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// 4. Boundary Constraints Validation/Enforcement
|
|
1438
|
+
if (shapeWidth > cellWidth_px) {
|
|
1439
|
+
shapeLeft = cellLeft_px
|
|
1440
|
+
} else {
|
|
1441
|
+
shapeLeft = Math.max(
|
|
1442
|
+
cellLeft_px,
|
|
1443
|
+
Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
|
|
1444
|
+
)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (shapeHeight > cellHeight_px) {
|
|
1448
|
+
shapeTop = cellTop_px
|
|
1449
|
+
} else {
|
|
1450
|
+
shapeTop = Math.max(
|
|
1451
|
+
cellTop_px,
|
|
1452
|
+
Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
} else {
|
|
1456
|
+
shapeLeft = config.x || 0
|
|
1457
|
+
shapeTop = config.y || 0
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// 5. Expand individual sub-elements / custom shapes
|
|
1160
1461
|
if (config.type === 'progressBar') {
|
|
1161
1462
|
const value = config.value !== undefined ? config.value : 0
|
|
1162
1463
|
const max = config.max !== undefined ? config.max : 100
|
|
1163
1464
|
const fill = config.fill || '#3B82F6'
|
|
1164
1465
|
const bgFill = config.backgroundFill || '#E5E7EB'
|
|
1165
|
-
const pbHeight = config.height !== undefined ? config.height : 8
|
|
1166
|
-
const pbWidth = config.width !== undefined ? config.width : cellWidth_px - 10
|
|
1167
|
-
|
|
1168
|
-
const pbX = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - pbWidth) / 2)
|
|
1169
|
-
const pbY = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - pbHeight) / 2)
|
|
1170
1466
|
|
|
1171
1467
|
const shapes = []
|
|
1172
1468
|
shapes.push({
|
|
1173
1469
|
type: 'roundedRectangle',
|
|
1174
1470
|
fill: bgFill,
|
|
1175
|
-
x:
|
|
1176
|
-
y:
|
|
1177
|
-
width:
|
|
1178
|
-
height:
|
|
1179
|
-
borderRadius:
|
|
1471
|
+
x: shapeLeft,
|
|
1472
|
+
y: shapeTop,
|
|
1473
|
+
width: shapeWidth,
|
|
1474
|
+
height: shapeHeight,
|
|
1475
|
+
borderRadius: shapeHeight / 2,
|
|
1180
1476
|
zIndex: config.zIndex,
|
|
1181
1477
|
})
|
|
1182
1478
|
|
|
1183
1479
|
const pct = Math.min(1, Math.max(0, value / max))
|
|
1184
1480
|
if (pct > 0) {
|
|
1185
|
-
const filledWidth =
|
|
1481
|
+
const filledWidth = shapeWidth * pct
|
|
1186
1482
|
shapes.push({
|
|
1187
1483
|
type: 'roundedRectangle',
|
|
1188
1484
|
fill: fill,
|
|
1189
|
-
x:
|
|
1190
|
-
y:
|
|
1485
|
+
x: shapeLeft,
|
|
1486
|
+
y: shapeTop,
|
|
1191
1487
|
width: filledWidth,
|
|
1192
|
-
height:
|
|
1193
|
-
borderRadius:
|
|
1488
|
+
height: shapeHeight,
|
|
1489
|
+
borderRadius: shapeHeight / 2,
|
|
1194
1490
|
zIndex: (config.zIndex || 0) + 1,
|
|
1195
1491
|
})
|
|
1196
1492
|
}
|
|
@@ -1200,23 +1496,15 @@ class TableManager {
|
|
|
1200
1496
|
if (config.type === 'badge') {
|
|
1201
1497
|
const text = String(config.text !== undefined ? config.text : '')
|
|
1202
1498
|
const fontSize = config.textStyle?.fontSize || 10
|
|
1203
|
-
const textWidth = text.length * fontSize * 0.6
|
|
1204
|
-
const paddingX = 12
|
|
1205
|
-
const badgeWidth = config.width !== undefined ? config.width : textWidth + paddingX * 2
|
|
1206
|
-
const badgeHeight = config.height !== undefined ? config.height : fontSize + 12
|
|
1207
|
-
|
|
1208
|
-
const x = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - badgeWidth) / 2)
|
|
1209
|
-
const y = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - badgeHeight) / 2)
|
|
1210
|
-
|
|
1211
1499
|
return [
|
|
1212
1500
|
{
|
|
1213
1501
|
type: 'roundedRectangle',
|
|
1214
1502
|
fill: config.fill || '#10B981',
|
|
1215
|
-
borderRadius:
|
|
1216
|
-
x:
|
|
1217
|
-
y:
|
|
1218
|
-
width:
|
|
1219
|
-
height:
|
|
1503
|
+
borderRadius: shapeHeight / 2,
|
|
1504
|
+
x: shapeLeft,
|
|
1505
|
+
y: shapeTop,
|
|
1506
|
+
width: shapeWidth,
|
|
1507
|
+
height: shapeHeight,
|
|
1220
1508
|
text: text,
|
|
1221
1509
|
textStyle: {
|
|
1222
1510
|
color: config.textStyle?.color || '#FFFFFF',
|
|
@@ -1234,9 +1522,8 @@ class TableManager {
|
|
|
1234
1522
|
}
|
|
1235
1523
|
|
|
1236
1524
|
if (config.type === 'icon') {
|
|
1237
|
-
const size = config.size || 16
|
|
1238
1525
|
const iconFill = config.fill
|
|
1239
|
-
const fontSize = Math.round(
|
|
1526
|
+
const fontSize = Math.round(shapeWidth * 0.8)
|
|
1240
1527
|
|
|
1241
1528
|
let baseConfig = null
|
|
1242
1529
|
switch (config.icon) {
|
|
@@ -1245,8 +1532,8 @@ class TableManager {
|
|
|
1245
1532
|
type: 'rectangle',
|
|
1246
1533
|
fill: 'none',
|
|
1247
1534
|
border: null,
|
|
1248
|
-
width:
|
|
1249
|
-
height:
|
|
1535
|
+
width: shapeWidth,
|
|
1536
|
+
height: shapeHeight,
|
|
1250
1537
|
text: '✔',
|
|
1251
1538
|
textStyle: {
|
|
1252
1539
|
color: iconFill || '#10B981',
|
|
@@ -1261,8 +1548,8 @@ class TableManager {
|
|
|
1261
1548
|
type: 'rectangle',
|
|
1262
1549
|
fill: 'none',
|
|
1263
1550
|
border: null,
|
|
1264
|
-
width:
|
|
1265
|
-
height:
|
|
1551
|
+
width: shapeWidth,
|
|
1552
|
+
height: shapeHeight,
|
|
1266
1553
|
text: '✘',
|
|
1267
1554
|
textStyle: {
|
|
1268
1555
|
color: iconFill || '#EF4444',
|
|
@@ -1277,8 +1564,8 @@ class TableManager {
|
|
|
1277
1564
|
type: 'triangle',
|
|
1278
1565
|
fill: iconFill || '#F59E0B',
|
|
1279
1566
|
border: null,
|
|
1280
|
-
width:
|
|
1281
|
-
height:
|
|
1567
|
+
width: shapeWidth,
|
|
1568
|
+
height: shapeHeight,
|
|
1282
1569
|
text: '!',
|
|
1283
1570
|
textStyle: {
|
|
1284
1571
|
color: '#FFFFFF',
|
|
@@ -1293,7 +1580,7 @@ class TableManager {
|
|
|
1293
1580
|
type: 'circle',
|
|
1294
1581
|
fill: iconFill || '#3B82F6',
|
|
1295
1582
|
border: null,
|
|
1296
|
-
radius:
|
|
1583
|
+
radius: shapeWidth / 2,
|
|
1297
1584
|
text: 'i',
|
|
1298
1585
|
textStyle: {
|
|
1299
1586
|
color: '#FFFFFF',
|
|
@@ -1308,8 +1595,8 @@ class TableManager {
|
|
|
1308
1595
|
type: 'star5',
|
|
1309
1596
|
fill: iconFill || '#FBBF24',
|
|
1310
1597
|
border: null,
|
|
1311
|
-
width:
|
|
1312
|
-
height:
|
|
1598
|
+
width: shapeWidth,
|
|
1599
|
+
height: shapeHeight,
|
|
1313
1600
|
}
|
|
1314
1601
|
break
|
|
1315
1602
|
case 'up':
|
|
@@ -1317,8 +1604,8 @@ class TableManager {
|
|
|
1317
1604
|
type: 'upArrow',
|
|
1318
1605
|
fill: iconFill || '#10B981',
|
|
1319
1606
|
border: null,
|
|
1320
|
-
width:
|
|
1321
|
-
height:
|
|
1607
|
+
width: shapeWidth,
|
|
1608
|
+
height: shapeHeight,
|
|
1322
1609
|
}
|
|
1323
1610
|
break
|
|
1324
1611
|
case 'down':
|
|
@@ -1326,8 +1613,8 @@ class TableManager {
|
|
|
1326
1613
|
type: 'downArrow',
|
|
1327
1614
|
fill: iconFill || '#EF4444',
|
|
1328
1615
|
border: null,
|
|
1329
|
-
width:
|
|
1330
|
-
height:
|
|
1616
|
+
width: shapeWidth,
|
|
1617
|
+
height: shapeHeight,
|
|
1331
1618
|
}
|
|
1332
1619
|
break
|
|
1333
1620
|
case 'arrow-right':
|
|
@@ -1335,8 +1622,8 @@ class TableManager {
|
|
|
1335
1622
|
type: 'rightArrow',
|
|
1336
1623
|
fill: iconFill || '#3B82F6',
|
|
1337
1624
|
border: null,
|
|
1338
|
-
width:
|
|
1339
|
-
height:
|
|
1625
|
+
width: shapeWidth,
|
|
1626
|
+
height: shapeHeight,
|
|
1340
1627
|
}
|
|
1341
1628
|
break
|
|
1342
1629
|
case 'arrow-left':
|
|
@@ -1344,19 +1631,16 @@ class TableManager {
|
|
|
1344
1631
|
type: 'leftArrow',
|
|
1345
1632
|
fill: iconFill || '#3B82F6',
|
|
1346
1633
|
border: null,
|
|
1347
|
-
width:
|
|
1348
|
-
height:
|
|
1634
|
+
width: shapeWidth,
|
|
1635
|
+
height: shapeHeight,
|
|
1349
1636
|
}
|
|
1350
1637
|
break
|
|
1351
1638
|
default:
|
|
1352
1639
|
return []
|
|
1353
1640
|
}
|
|
1354
1641
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
baseConfig.x = x
|
|
1359
|
-
baseConfig.y = y
|
|
1642
|
+
baseConfig.x = shapeLeft
|
|
1643
|
+
baseConfig.y = shapeTop
|
|
1360
1644
|
baseConfig.zIndex = config.zIndex
|
|
1361
1645
|
if (config.border) baseConfig.border = config.border
|
|
1362
1646
|
if (config.transparency !== undefined) baseConfig.transparency = config.transparency
|
|
@@ -1366,58 +1650,11 @@ class TableManager {
|
|
|
1366
1650
|
return [baseConfig]
|
|
1367
1651
|
}
|
|
1368
1652
|
|
|
1369
|
-
const shapeWidth =
|
|
1370
|
-
config.width !== undefined
|
|
1371
|
-
? config.width
|
|
1372
|
-
: config.size !== undefined
|
|
1373
|
-
? config.size
|
|
1374
|
-
: config.radius !== undefined
|
|
1375
|
-
? config.radius * 2
|
|
1376
|
-
: 12
|
|
1377
|
-
const shapeHeight =
|
|
1378
|
-
config.height !== undefined
|
|
1379
|
-
? config.height
|
|
1380
|
-
: config.size !== undefined
|
|
1381
|
-
? config.size
|
|
1382
|
-
: config.radius !== undefined
|
|
1383
|
-
? config.radius * 2
|
|
1384
|
-
: 12
|
|
1385
|
-
|
|
1386
|
-
let shapeLeft = cellLeft_px
|
|
1387
|
-
let shapeTop = cellTop_px
|
|
1388
|
-
|
|
1389
|
-
if (config.position === 'center') {
|
|
1390
|
-
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
|
|
1391
|
-
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
|
|
1392
|
-
} else if (config.position === 'left') {
|
|
1393
|
-
shapeLeft = cellLeft_px + 5
|
|
1394
|
-
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
|
|
1395
|
-
} else if (config.position === 'right') {
|
|
1396
|
-
shapeLeft = cellLeft_px + cellWidth_px - shapeWidth - 5
|
|
1397
|
-
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
|
|
1398
|
-
} else if (config.position === 'top') {
|
|
1399
|
-
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
|
|
1400
|
-
shapeTop = cellTop_px + 5
|
|
1401
|
-
} else if (config.position === 'bottom') {
|
|
1402
|
-
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
|
|
1403
|
-
shapeTop = cellTop_px + cellHeight_px - shapeHeight - 5
|
|
1404
|
-
} else {
|
|
1405
|
-
if (config.x === undefined && config.y === undefined) {
|
|
1406
|
-
shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
|
|
1407
|
-
shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
if (config.x !== undefined) {
|
|
1412
|
-
shapeLeft += config.x
|
|
1413
|
-
}
|
|
1414
|
-
if (config.y !== undefined) {
|
|
1415
|
-
shapeTop += config.y
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
1653
|
const expanded = Object.assign({}, config, {
|
|
1419
1654
|
x: shapeLeft,
|
|
1420
1655
|
y: shapeTop,
|
|
1656
|
+
width: shapeWidth,
|
|
1657
|
+
height: shapeHeight,
|
|
1421
1658
|
})
|
|
1422
1659
|
|
|
1423
1660
|
if (expanded.type === 'circle' && expanded.radius === undefined) {
|
|
@@ -1458,6 +1695,8 @@ class TableManager {
|
|
|
1458
1695
|
}
|
|
1459
1696
|
}
|
|
1460
1697
|
|
|
1698
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
|
|
1699
|
+
|
|
1461
1700
|
const xfrm = frameObj['p:xfrm']
|
|
1462
1701
|
const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
|
|
1463
1702
|
const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
|
|
@@ -1561,6 +1800,69 @@ class TableManager {
|
|
|
1561
1800
|
})
|
|
1562
1801
|
}
|
|
1563
1802
|
|
|
1803
|
+
getTableRows(slideIndex, tableId, options = {}, slideManager) {
|
|
1804
|
+
const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
1805
|
+
const trs = tblObj['a:tr'] || []
|
|
1806
|
+
if (trs.length === 0) {
|
|
1807
|
+
return options.includeMetadata
|
|
1808
|
+
? { rows: [], rowCount: 0, columnCount: 0, mergedCells: [] }
|
|
1809
|
+
: []
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const numRows = trs.length
|
|
1813
|
+
const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
|
|
1814
|
+
const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
|
|
1815
|
+
const numCols = gridColsArr.length
|
|
1816
|
+
|
|
1817
|
+
// Extract all raw cell text, resolving merges to their parent's text
|
|
1818
|
+
const matrix = []
|
|
1819
|
+
for (let r = 0; r < numRows; r++) {
|
|
1820
|
+
const rowCells = []
|
|
1821
|
+
for (let c = 0; c < numCols; c++) {
|
|
1822
|
+
const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
|
|
1823
|
+
const cell = trs[parent.row]?.['a:tc']?.[parent.col]
|
|
1824
|
+
const text = cell ? this.#getCellText(cell) : ''
|
|
1825
|
+
rowCells.push(text)
|
|
1826
|
+
}
|
|
1827
|
+
matrix.push(rowCells)
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Header names are extracted from the first row (index 0)
|
|
1831
|
+
const headerNames = matrix[0].map((hText, cIdx) => {
|
|
1832
|
+
const cleaned = hText.trim()
|
|
1833
|
+
return cleaned || `column${cIdx + 1}`
|
|
1834
|
+
})
|
|
1835
|
+
|
|
1836
|
+
// Compute the data rows (excluding the header row at index 0)
|
|
1837
|
+
const dataRows = matrix.slice(1)
|
|
1838
|
+
|
|
1839
|
+
let rowsResult = []
|
|
1840
|
+
if (options.raw) {
|
|
1841
|
+
rowsResult = dataRows
|
|
1842
|
+
} else {
|
|
1843
|
+
for (const rowCells of dataRows) {
|
|
1844
|
+
const rowObj = {}
|
|
1845
|
+
for (let c = 0; c < numCols; c++) {
|
|
1846
|
+
const key = headerNames[c]
|
|
1847
|
+
rowObj[key] = rowCells[c] || ''
|
|
1848
|
+
}
|
|
1849
|
+
rowsResult.push(rowObj)
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
if (options.includeMetadata) {
|
|
1854
|
+
const mergedCells = this.getMergedCells(slideIndex, tableId, slideManager)
|
|
1855
|
+
return {
|
|
1856
|
+
rows: rowsResult,
|
|
1857
|
+
rowCount: numRows,
|
|
1858
|
+
columnCount: numCols,
|
|
1859
|
+
mergedCells,
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
return rowsResult
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1564
1866
|
addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
|
|
1565
1867
|
const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
|
|
1566
1868
|
slideIndex,
|
|
@@ -1568,6 +1870,8 @@ class TableManager {
|
|
|
1568
1870
|
slideManager
|
|
1569
1871
|
)
|
|
1570
1872
|
|
|
1873
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
|
|
1874
|
+
|
|
1571
1875
|
const xfrm = frameObj['p:xfrm']
|
|
1572
1876
|
const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
|
|
1573
1877
|
const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
|
|
@@ -1651,6 +1955,8 @@ class TableManager {
|
|
|
1651
1955
|
slideManager
|
|
1652
1956
|
)
|
|
1653
1957
|
|
|
1958
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
|
|
1959
|
+
|
|
1654
1960
|
const shapes = shapeManager.getShapes(slideIndex, slideManager)
|
|
1655
1961
|
const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
|
|
1656
1962
|
const matchingShapes = shapes.filter(
|
|
@@ -1786,6 +2092,297 @@ class TableManager {
|
|
|
1786
2092
|
}
|
|
1787
2093
|
}
|
|
1788
2094
|
|
|
2095
|
+
#calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
|
|
2096
|
+
const trsArr = tblObj['a:tr'] || []
|
|
2097
|
+
if (trsArr.length === 0) return []
|
|
2098
|
+
|
|
2099
|
+
const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
|
|
2100
|
+
const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
|
|
2101
|
+
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
2102
|
+
|
|
2103
|
+
const numRows = trsArr.length
|
|
2104
|
+
const numCols = colWidths.length
|
|
2105
|
+
|
|
2106
|
+
// Initialize rowHeights with original height or 0
|
|
2107
|
+
const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
|
|
2108
|
+
|
|
2109
|
+
// Helper to get paragraph font size
|
|
2110
|
+
const getParagraphFontSize = p => {
|
|
2111
|
+
let maxSz = 14 // default 14pt
|
|
2112
|
+
if (p['a:pPr']?.['a:defRPr']?.['@_sz']) {
|
|
2113
|
+
maxSz = parseInt(p['a:pPr']['a:defRPr']['@_sz'], 10) / 100
|
|
2114
|
+
}
|
|
2115
|
+
if (p['a:r']) {
|
|
2116
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
2117
|
+
for (const r of runs) {
|
|
2118
|
+
if (r['a:rPr']?.['@_sz']) {
|
|
2119
|
+
const szVal = parseInt(r['a:rPr']['@_sz'], 10) / 100
|
|
2120
|
+
if (szVal > maxSz) {
|
|
2121
|
+
maxSz = szVal
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return maxSz
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Helper to wrap text
|
|
2130
|
+
const wrapText = (text, availWidth_px, fontSize) => {
|
|
2131
|
+
const charWidth = fontSize * 0.65
|
|
2132
|
+
const words = text.split(/(\s+)/)
|
|
2133
|
+
let linesCount = 0
|
|
2134
|
+
let currentLineLen = 0
|
|
2135
|
+
|
|
2136
|
+
for (const word of words) {
|
|
2137
|
+
if (!word) continue
|
|
2138
|
+
const wordWidth = word.length * charWidth
|
|
2139
|
+
if (wordWidth > availWidth_px) {
|
|
2140
|
+
if (currentLineLen > 0) {
|
|
2141
|
+
linesCount++
|
|
2142
|
+
currentLineLen = 0
|
|
2143
|
+
}
|
|
2144
|
+
let remainingWidth = wordWidth
|
|
2145
|
+
while (remainingWidth > 0) {
|
|
2146
|
+
linesCount++
|
|
2147
|
+
remainingWidth -= availWidth_px
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
if (currentLineLen + wordWidth > availWidth_px) {
|
|
2151
|
+
linesCount++
|
|
2152
|
+
currentLineLen = word.trim() ? wordWidth : 0
|
|
2153
|
+
} else {
|
|
2154
|
+
currentLineLen += wordWidth
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (currentLineLen > 0 || linesCount === 0) {
|
|
2159
|
+
linesCount++
|
|
2160
|
+
}
|
|
2161
|
+
return linesCount
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// Helper to get cell margins
|
|
2165
|
+
const getCellMargins = cell => {
|
|
2166
|
+
const tcPr = cell['a:tcPr']
|
|
2167
|
+
const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
|
|
2168
|
+
const marR = tcPr?.['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
|
|
2169
|
+
const marT = tcPr?.['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
|
|
2170
|
+
const marB = tcPr?.['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
|
|
2171
|
+
return { marL, marR, marT, marB }
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// Calculate required height for each cell
|
|
2175
|
+
const cellHeights = Array.from({ length: numRows }, () => new Array(numCols).fill(0))
|
|
2176
|
+
|
|
2177
|
+
for (let r = 0; r < numRows; r++) {
|
|
2178
|
+
const row = trsArr[r]
|
|
2179
|
+
const tcs = row['a:tc'] || []
|
|
2180
|
+
for (let c = 0; c < numCols; c++) {
|
|
2181
|
+
const cell = tcs[c]
|
|
2182
|
+
if (!cell || cell['@_hMerge'] || cell['@_vMerge']) continue
|
|
2183
|
+
|
|
2184
|
+
const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
|
|
2185
|
+
const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
|
|
2186
|
+
|
|
2187
|
+
// Calculate cell width
|
|
2188
|
+
let cellWidth = 0
|
|
2189
|
+
for (let idx = 0; idx < gridSpan; idx++) {
|
|
2190
|
+
cellWidth += colWidths[parent.col + idx] || 0
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const { marL, marR, marT, marB } = getCellMargins(cell)
|
|
2194
|
+
const availWidth = cellWidth - marL - marR
|
|
2195
|
+
const availWidth_px = Math.max(1, availWidth / 9525)
|
|
2196
|
+
|
|
2197
|
+
// Calculate text height
|
|
2198
|
+
const txBody = cell['a:txBody']
|
|
2199
|
+
let textHeight_emu = 0
|
|
2200
|
+
if (txBody) {
|
|
2201
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
2202
|
+
for (const p of paras) {
|
|
2203
|
+
const fontSize = getParagraphFontSize(p)
|
|
2204
|
+
let pText = ''
|
|
2205
|
+
if (p['a:r']) {
|
|
2206
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
2207
|
+
for (const r of runs) {
|
|
2208
|
+
if (r['a:t']) {
|
|
2209
|
+
pText += String(r['a:t'])
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const linesCount = wrapText(pText, availWidth_px, fontSize)
|
|
2215
|
+
const lineHeight_emu = fontSize * 20780 // 1.4 line height multiplier
|
|
2216
|
+
|
|
2217
|
+
let pHeight_emu = linesCount * lineHeight_emu
|
|
2218
|
+
if (p['a:pPr']?.['a:spcBef']?.['a:spcPts']?.['@_val']) {
|
|
2219
|
+
pHeight_emu += parseInt(p['a:pPr']['a:spcBef']['a:spcPts']['@_val'], 10) * 127
|
|
2220
|
+
}
|
|
2221
|
+
if (p['a:pPr']?.['a:spcAft']?.['a:spcPts']?.['@_val']) {
|
|
2222
|
+
pHeight_emu += parseInt(p['a:pPr']['a:spcAft']['a:spcPts']['@_val'], 10) * 127
|
|
2223
|
+
}
|
|
2224
|
+
textHeight_emu += pHeight_emu
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
const totalCellHeight_emu = marT + marB + textHeight_emu
|
|
2229
|
+
cellHeights[r][c] = totalCellHeight_emu
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Now resolve row heights based on required cell heights
|
|
2234
|
+
// First, non-vertically-merged cells define row heights directly
|
|
2235
|
+
for (let r = 0; r < numRows; r++) {
|
|
2236
|
+
let maxCellHeight = rowHeights[r] // Start with original template height as floor
|
|
2237
|
+
const row = trsArr[r]
|
|
2238
|
+
const tcs = row['a:tc'] || []
|
|
2239
|
+
for (let c = 0; c < numCols; c++) {
|
|
2240
|
+
const cell = tcs[c]
|
|
2241
|
+
if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
|
|
2242
|
+
const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
|
|
2243
|
+
if (rowSpan === 1) {
|
|
2244
|
+
if (cellHeights[r][c] > maxCellHeight) {
|
|
2245
|
+
maxCellHeight = cellHeights[r][c]
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
rowHeights[r] = maxCellHeight
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// Next, adjust for vertically merged cells (rowSpan > 1)
|
|
2253
|
+
for (let r = 0; r < numRows; r++) {
|
|
2254
|
+
const row = trsArr[r]
|
|
2255
|
+
const tcs = row['a:tc'] || []
|
|
2256
|
+
for (let c = 0; c < numCols; c++) {
|
|
2257
|
+
const cell = tcs[c]
|
|
2258
|
+
if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
|
|
2259
|
+
const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
|
|
2260
|
+
if (rowSpan > 1) {
|
|
2261
|
+
const reqHeight = cellHeights[r][c]
|
|
2262
|
+
// Sum currently allocated row heights for spanned rows
|
|
2263
|
+
let currentSpanHeight = 0
|
|
2264
|
+
for (let idx = 0; idx < rowSpan; idx++) {
|
|
2265
|
+
currentSpanHeight += rowHeights[r + idx] || 0
|
|
2266
|
+
}
|
|
2267
|
+
if (reqHeight > currentSpanHeight) {
|
|
2268
|
+
// Distribute the extra required height equally across all spanned rows
|
|
2269
|
+
const diff = reqHeight - currentSpanHeight
|
|
2270
|
+
const extraPerRow = Math.ceil(diff / rowSpan)
|
|
2271
|
+
for (let idx = 0; idx < rowSpan; idx++) {
|
|
2272
|
+
rowHeights[r + idx] += extraPerRow
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Update row heights in XML
|
|
2280
|
+
for (let r = 0; r < numRows; r++) {
|
|
2281
|
+
trsArr[r]['@_h'] = String(rowHeights[r])
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
return rowHeights
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
#getNestedHeight(val) {
|
|
2288
|
+
if (Array.isArray(val)) {
|
|
2289
|
+
if (val.length === 0) return 1
|
|
2290
|
+
return val.reduce((sum, item) => sum + this.#getNestedHeight(item), 0)
|
|
2291
|
+
}
|
|
2292
|
+
return 1
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
#expandCellVal(val, targetHeight) {
|
|
2296
|
+
if (!Array.isArray(val)) {
|
|
2297
|
+
const res = []
|
|
2298
|
+
res.push({ value: val !== undefined ? val : '', rowSpan: targetHeight })
|
|
2299
|
+
for (let i = 1; i < targetHeight; i++) {
|
|
2300
|
+
res.push({ vMerge: true })
|
|
2301
|
+
}
|
|
2302
|
+
return res
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
if (val.length === 0) {
|
|
2306
|
+
const res = []
|
|
2307
|
+
res.push({ value: '', rowSpan: targetHeight })
|
|
2308
|
+
for (let i = 1; i < targetHeight; i++) {
|
|
2309
|
+
res.push({ vMerge: true })
|
|
2310
|
+
}
|
|
2311
|
+
return res
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const itemHeights = val.map(item => this.#getNestedHeight(item))
|
|
2315
|
+
const currentSum = itemHeights.reduce((a, b) => a + b, 0)
|
|
2316
|
+
|
|
2317
|
+
const allocatedHeights = []
|
|
2318
|
+
let remaining = targetHeight
|
|
2319
|
+
for (let i = 0; i < val.length; i++) {
|
|
2320
|
+
const share = Math.round((itemHeights[i] / currentSum) * targetHeight)
|
|
2321
|
+
allocatedHeights.push(share)
|
|
2322
|
+
remaining -= share
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
if (remaining !== 0) {
|
|
2326
|
+
let idx = 0
|
|
2327
|
+
while (remaining > 0) {
|
|
2328
|
+
allocatedHeights[idx % allocatedHeights.length]++
|
|
2329
|
+
remaining--
|
|
2330
|
+
idx++
|
|
2331
|
+
}
|
|
2332
|
+
while (remaining < 0) {
|
|
2333
|
+
let reduced = false
|
|
2334
|
+
for (let i = 0; i < allocatedHeights.length; i++) {
|
|
2335
|
+
const actualIdx = (idx + i) % allocatedHeights.length
|
|
2336
|
+
if (allocatedHeights[actualIdx] > 1) {
|
|
2337
|
+
allocatedHeights[actualIdx]--
|
|
2338
|
+
remaining++
|
|
2339
|
+
reduced = true
|
|
2340
|
+
break
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
if (!reduced) break
|
|
2344
|
+
idx++
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
const result = []
|
|
2349
|
+
for (let i = 0; i < val.length; i++) {
|
|
2350
|
+
result.push(...this.#expandCellVal(val[i], allocatedHeights[i]))
|
|
2351
|
+
}
|
|
2352
|
+
return result
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
#applyAutoMerge(cells) {
|
|
2356
|
+
const result = [...cells]
|
|
2357
|
+
let i = 0
|
|
2358
|
+
while (i < result.length) {
|
|
2359
|
+
const cell = result[i]
|
|
2360
|
+
if (cell.vMerge) {
|
|
2361
|
+
i++
|
|
2362
|
+
continue
|
|
2363
|
+
}
|
|
2364
|
+
let count = 1
|
|
2365
|
+
let j = i + 1
|
|
2366
|
+
while (
|
|
2367
|
+
j < result.length &&
|
|
2368
|
+
!result[j].vMerge &&
|
|
2369
|
+
result[j].value === cell.value &&
|
|
2370
|
+
cell.value !== ''
|
|
2371
|
+
) {
|
|
2372
|
+
count++
|
|
2373
|
+
j++
|
|
2374
|
+
}
|
|
2375
|
+
if (count > 1) {
|
|
2376
|
+
cell.rowSpan = count
|
|
2377
|
+
for (let k = i + 1; k < j; k++) {
|
|
2378
|
+
result[k] = { vMerge: true }
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
i = j
|
|
2382
|
+
}
|
|
2383
|
+
return result
|
|
2384
|
+
}
|
|
2385
|
+
|
|
1789
2386
|
#generateRandomUint32() {
|
|
1790
2387
|
return Math.floor(Math.random() * 4294967296)
|
|
1791
2388
|
}
|