node-pptx-templater 1.1.3 → 1.1.4

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.
@@ -48,6 +48,19 @@ class TableManager {
48
48
  /** @private @type {XMLParser} */
49
49
  #xmlParser
50
50
 
51
+ /**
52
+ * In-memory registry mapping cellshape names to their original config.
53
+ * Used to reposition shapes after table structure mutations (row removal,
54
+ * insertion, merge, etc.).
55
+ *
56
+ * Key: shape name (e.g. "cellshape_Table_2_5_0")
57
+ * Value: { slideIndex, resolvedTableId, tableId, rowIndex, colIndex, shapeIndex, config }
58
+ *
59
+ * @private
60
+ * @type {Map<string, {slideIndex: number, resolvedTableId: string, tableId: string, rowIndex: number, colIndex: number, shapeIndex: string|number, config: Object}>}
61
+ */
62
+ #cellShapeAnchors = new Map()
63
+
51
64
  /**
52
65
  * @param {XMLParser} xmlParser
53
66
  */
@@ -253,7 +266,15 @@ class TableManager {
253
266
  * @param {string[]} rowData
254
267
  * @param {SlideManager} slideManager
255
268
  */
256
- addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
269
+ addTableRow(slideIndex, tableId, rowData, slideManager, shapeManagerOrOptions, options = {}) {
270
+ let shapeManager = null
271
+ let actualOptions = options
272
+ if (shapeManagerOrOptions && typeof shapeManagerOrOptions.getShapes === 'function') {
273
+ shapeManager = shapeManagerOrOptions
274
+ } else if (shapeManagerOrOptions && typeof shapeManagerOrOptions === 'object') {
275
+ actualOptions = shapeManagerOrOptions
276
+ }
277
+
257
278
  const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
258
279
 
259
280
  const trs = tblObj['a:tr'] || []
@@ -261,6 +282,10 @@ class TableManager {
261
282
  throw new PPTXError('No rows to clone from')
262
283
  }
263
284
 
285
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
286
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
287
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
288
+
264
289
  const lastRow = trs[trs.length - 1]
265
290
  const numCols = lastRow['a:tc']?.length || 0
266
291
 
@@ -273,7 +298,7 @@ class TableManager {
273
298
 
274
299
  // Expand each column value to targetHeight
275
300
  const expandedCols = []
276
- const strategy = options.mergeStrategy || 'auto'
301
+ const strategy = actualOptions.mergeStrategy || 'auto'
277
302
  for (let c = 0; c < numCols; c++) {
278
303
  let colCells = this.#expandCellVal(rowData[c], targetHeight)
279
304
  if (strategy === 'none') {
@@ -290,6 +315,12 @@ class TableManager {
290
315
  expandedCols.push(colCells)
291
316
  }
292
317
 
318
+ const startRowIndex = trs.length
319
+ const shapeCellsToCreate = []
320
+ const isShapeConfig = val => {
321
+ return val && typeof val === 'object' && typeof val.type === 'string'
322
+ }
323
+
293
324
  // Clone and append rows
294
325
  for (let r = 0; r < targetHeight; r++) {
295
326
  const newRow = this.#xmlParser.deepClone(lastRow)
@@ -312,10 +343,117 @@ class TableManager {
312
343
  } else {
313
344
  let text = cellDef.value
314
345
  let cellOpts = {}
315
- if (cellDef.value && typeof cellDef.value === 'object') {
346
+
347
+ if (isShapeConfig(cellDef.value)) {
348
+ const config = cellDef.value
349
+ const globalRowIndex = startRowIndex + r
350
+
351
+ const isStandardShape = [
352
+ 'circle',
353
+ 'square',
354
+ 'rectangle',
355
+ 'triangle',
356
+ 'diamond',
357
+ 'hexagon',
358
+ 'line',
359
+ ].includes(config.type)
360
+
361
+ const shapeConfig = { ...config }
362
+
363
+ if (isStandardShape) {
364
+ text =
365
+ config.text !== undefined
366
+ ? config.text
367
+ : config.value !== undefined
368
+ ? config.value
369
+ : ''
370
+ delete shapeConfig.text // DO NOT render text inside the shape overlay!
371
+ } else {
372
+ text = '' // for badges/icons/progressBars, cell text is empty!
373
+ }
374
+
375
+ shapeCellsToCreate.push({
376
+ rowIndex: globalRowIndex,
377
+ colIndex: c,
378
+ config: shapeConfig,
379
+ })
380
+
381
+ cellOpts = {}
382
+ if (config.cellFill) cellOpts.fill = config.cellFill
383
+ if (config.cellAlign) cellOpts.align = config.cellAlign
384
+
385
+ // Estimate shape dimensions to set margins
386
+ const colWidth_emu = colWidths[c] || 0
387
+ const colWidth_px = colWidth_emu / 9525
388
+
389
+ const parseLength = (val, maxVal) => {
390
+ if (typeof val === 'string' && val.endsWith('%')) {
391
+ return (parseFloat(val) / 100) * maxVal
392
+ }
393
+ return val !== undefined ? parseFloat(val) : undefined
394
+ }
395
+
396
+ let shapeWidth = 12
397
+ let shapeHeight = 12
398
+
399
+ if (config.width !== undefined) {
400
+ shapeWidth = parseLength(config.width, colWidth_px) || 12
401
+ } else if (config.size !== undefined) {
402
+ shapeWidth = parseLength(config.size, colWidth_px) || 12
403
+ } else if (config.radius !== undefined) {
404
+ shapeWidth = (parseLength(config.radius, colWidth_px) || 6) * 2
405
+ }
406
+
407
+ if (config.height !== undefined) {
408
+ shapeHeight = parseLength(config.height, 50) || 12
409
+ } else if (config.size !== undefined) {
410
+ shapeHeight = parseLength(config.size, 50) || 12
411
+ } else if (config.radius !== undefined) {
412
+ shapeHeight = (parseLength(config.radius, 25) || 6) * 2
413
+ }
414
+
415
+ if (isStandardShape && text !== '') {
416
+ const position = config.position || (config.text ? 'left' : 'center')
417
+ const tcPr = tcObj['a:tcPr'] || {}
418
+ const currentMarL =
419
+ tcPr['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
420
+ const currentMarR =
421
+ tcPr['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
422
+ const currentMarT =
423
+ tcPr['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
424
+ const currentMarB =
425
+ tcPr['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
426
+
427
+ tcObj['a:tcPr'] = tcObj['a:tcPr'] || {}
428
+
429
+ const isLeft = position.includes('left') || position === 'left'
430
+ const isRight = position.includes('right') || position === 'right'
431
+ const isTop = position === 'top' || position.startsWith('top-')
432
+ const isBottom = position === 'bottom' || position.startsWith('bottom-')
433
+
434
+ if (isLeft) {
435
+ tcObj['a:tcPr']['@_marL'] = String(
436
+ currentMarL + Math.round(shapeWidth * 9525) + 57150
437
+ )
438
+ } else if (isRight) {
439
+ tcObj['a:tcPr']['@_marR'] = String(
440
+ currentMarR + Math.round(shapeWidth * 9525) + 57150
441
+ )
442
+ } else if (isTop) {
443
+ tcObj['a:tcPr']['@_marT'] = String(
444
+ currentMarT + Math.round(shapeHeight * 9525) + 57150
445
+ )
446
+ } else if (isBottom) {
447
+ tcObj['a:tcPr']['@_marB'] = String(
448
+ currentMarB + Math.round(shapeHeight * 9525) + 57150
449
+ )
450
+ }
451
+ }
452
+ } else if (cellDef.value && typeof cellDef.value === 'object') {
316
453
  text = cellDef.value.value !== undefined ? cellDef.value.value : ''
317
454
  cellOpts = cellDef.value
318
455
  }
456
+
319
457
  this.#setCellTextObj(tcObj, text)
320
458
  if (cellDef.rowSpan && cellDef.rowSpan > 1 && strategy !== 'none') {
321
459
  tcObj['@_rowSpan'] = String(cellDef.rowSpan)
@@ -327,6 +465,25 @@ class TableManager {
327
465
  }
328
466
 
329
467
  slideManager.markSlideObjDirty(slideIndex)
468
+
469
+ if (shapeCellsToCreate.length > 0 && shapeManager) {
470
+ for (const item of shapeCellsToCreate) {
471
+ const resolvedConfig = { ...item.config }
472
+ if (!resolvedConfig.position) {
473
+ resolvedConfig.position = resolvedConfig.text ? 'left' : 'center'
474
+ }
475
+
476
+ this.addCellShape(
477
+ slideIndex,
478
+ tableId,
479
+ item.rowIndex,
480
+ item.colIndex,
481
+ resolvedConfig,
482
+ slideManager,
483
+ shapeManager
484
+ )
485
+ }
486
+ }
330
487
  }
331
488
 
332
489
  /**
@@ -337,8 +494,8 @@ class TableManager {
337
494
  * @param {number} rowIndex - 0-based row index.
338
495
  * @param {SlideManager} slideManager
339
496
  */
340
- removeTableRow(slideIndex, tableId, rowIndex, slideManager) {
341
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
497
+ removeTableRow(slideIndex, tableId, rowIndex, slideManager, shapeManager = null) {
498
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
342
499
 
343
500
  const trs = tblObj['a:tr'] || []
344
501
  if (rowIndex < 0 || rowIndex >= trs.length) {
@@ -348,6 +505,18 @@ class TableManager {
348
505
  trs.splice(rowIndex, 1)
349
506
 
350
507
  slideManager.markSlideObjDirty(slideIndex)
508
+
509
+ if (shapeManager) {
510
+ this.#adjustCellShapesAfterRowShift(
511
+ slideIndex,
512
+ resolvedTableId,
513
+ tableId,
514
+ rowIndex,
515
+ -1,
516
+ slideManager,
517
+ shapeManager
518
+ )
519
+ }
351
520
  }
352
521
 
353
522
  /**
@@ -359,8 +528,8 @@ class TableManager {
359
528
  * @param {string[]} rowData
360
529
  * @param {SlideManager} slideManager
361
530
  */
362
- insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager) {
363
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
531
+ insertTableRow(slideIndex, tableId, rowIndex, rowData, slideManager, shapeManager = null) {
532
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
364
533
 
365
534
  const trs = tblObj['a:tr'] || []
366
535
  if (rowIndex < 0 || rowIndex > trs.length) {
@@ -387,6 +556,18 @@ class TableManager {
387
556
  trs.splice(rowIndex, 0, newRow)
388
557
 
389
558
  slideManager.markSlideObjDirty(slideIndex)
559
+
560
+ if (shapeManager) {
561
+ this.#adjustCellShapesAfterRowShift(
562
+ slideIndex,
563
+ resolvedTableId,
564
+ tableId,
565
+ rowIndex,
566
+ +1,
567
+ slideManager,
568
+ shapeManager
569
+ )
570
+ }
390
571
  }
391
572
 
392
573
  /**
@@ -398,8 +579,15 @@ class TableManager {
398
579
  * @param {number} targetRowIndex
399
580
  * @param {SlideManager} slideManager
400
581
  */
401
- cloneTableRow(slideIndex, tableId, sourceRowIndex, targetRowIndex, slideManager) {
402
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
582
+ cloneTableRow(
583
+ slideIndex,
584
+ tableId,
585
+ sourceRowIndex,
586
+ targetRowIndex,
587
+ slideManager,
588
+ shapeManager = null
589
+ ) {
590
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
403
591
 
404
592
  const trs = tblObj['a:tr'] || []
405
593
  if (sourceRowIndex < 0 || sourceRowIndex >= trs.length) {
@@ -416,6 +604,18 @@ class TableManager {
416
604
  trs.splice(targetRowIndex, 0, newRow)
417
605
 
418
606
  slideManager.markSlideObjDirty(slideIndex)
607
+
608
+ if (shapeManager) {
609
+ this.#adjustCellShapesAfterRowShift(
610
+ slideIndex,
611
+ resolvedTableId,
612
+ tableId,
613
+ targetRowIndex,
614
+ +1,
615
+ slideManager,
616
+ shapeManager
617
+ )
618
+ }
419
619
  }
420
620
 
421
621
  /**
@@ -563,7 +763,16 @@ class TableManager {
563
763
  * @param {number} endCol
564
764
  * @param {SlideManager} slideManager
565
765
  */
566
- mergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
766
+ mergeCells(
767
+ slideIndex,
768
+ tableId,
769
+ startRow,
770
+ startCol,
771
+ endRow,
772
+ endCol,
773
+ slideManager,
774
+ shapeManager = null
775
+ ) {
567
776
  const validation = this.validateMergeRegion(
568
777
  slideIndex,
569
778
  tableId,
@@ -577,7 +786,7 @@ class TableManager {
577
786
  throw new PPTXError(`Invalid merge region: ${validation.errors.join('; ')}`)
578
787
  }
579
788
 
580
- const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
789
+ const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
581
790
  const trs = tblObj['a:tr'] || []
582
791
 
583
792
  const allTexts = []
@@ -630,6 +839,20 @@ class TableManager {
630
839
  this.#setCellTextObj(originCell, combinedText)
631
840
 
632
841
  slideManager.markSlideObjDirty(slideIndex)
842
+
843
+ if (shapeManager) {
844
+ this.#repositionCellShapesInRegion(
845
+ slideIndex,
846
+ tableId,
847
+ resolvedTableId,
848
+ startRow,
849
+ startCol,
850
+ endRow,
851
+ endCol,
852
+ slideManager,
853
+ shapeManager
854
+ )
855
+ }
633
856
  }
634
857
 
635
858
  /**
@@ -643,7 +866,16 @@ class TableManager {
643
866
  * @param {number} endCol
644
867
  * @param {SlideManager} slideManager
645
868
  */
646
- unmergeCells(slideIndex, tableId, startRow, startCol, endRow, endCol, slideManager) {
869
+ unmergeCells(
870
+ slideIndex,
871
+ tableId,
872
+ startRow,
873
+ startCol,
874
+ endRow,
875
+ endCol,
876
+ slideManager,
877
+ shapeManager = null
878
+ ) {
647
879
  let actualSlideManager = slideManager
648
880
  let actualEndRow = endRow
649
881
  let actualEndCol = endCol
@@ -679,6 +911,21 @@ class TableManager {
679
911
  if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
680
912
  }
681
913
  }
914
+
915
+ if (shapeManager) {
916
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, actualSlideManager)
917
+ this.#repositionCellShapesInRegion(
918
+ slideIndex,
919
+ tableId,
920
+ resolvedTableId,
921
+ R.startRow,
922
+ R.startCol,
923
+ R.endRow,
924
+ R.endCol,
925
+ actualSlideManager,
926
+ shapeManager
927
+ )
928
+ }
682
929
  } else {
683
930
  for (let r = startRow; r <= actualEndRow; r++) {
684
931
  const rowObj = trs[r]
@@ -693,6 +940,21 @@ class TableManager {
693
940
  if (cell['@_rowSpan'] !== undefined) delete cell['@_rowSpan']
694
941
  }
695
942
  }
943
+
944
+ if (shapeManager) {
945
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, actualSlideManager)
946
+ this.#repositionCellShapesInRegion(
947
+ slideIndex,
948
+ tableId,
949
+ resolvedTableId,
950
+ startRow,
951
+ startCol,
952
+ actualEndRow,
953
+ actualEndCol,
954
+ actualSlideManager,
955
+ shapeManager
956
+ )
957
+ }
696
958
  }
697
959
 
698
960
  actualSlideManager.markSlideObjDirty(slideIndex)
@@ -714,25 +976,89 @@ class TableManager {
714
976
  }
715
977
 
716
978
  const trs = tblObj['a:tr'] || []
979
+ const numRows = trs.length
980
+ if (numRows === 0) return []
981
+ const numCols = trs[0]['a:tc']?.length || 0
982
+
717
983
  const merged = []
984
+ const visited = Array.from({ length: numRows }, () => Array(numCols).fill(false))
718
985
 
719
- for (let r = 0; r < trs.length; r++) {
986
+ for (let r = 0; r < numRows; r++) {
720
987
  const row = trs[r]
721
988
  const tcs = row['a:tc'] || []
722
989
  for (let c = 0; c < tcs.length; c++) {
990
+ if (visited[r][c]) continue
723
991
  const cell = tcs[c]
724
992
  if (!cell) continue
725
993
 
726
- const gridSpan = parseInt(cell['@_gridSpan'] || 1, 10)
727
- const rowSpan = parseInt(cell['@_rowSpan'] || 1, 10)
994
+ const isVMerged =
995
+ cell['@_vMerge'] === '1' || cell['@_vMerge'] === 'true' || cell['@_vMerge'] === true
996
+ const isHMerged =
997
+ cell['@_hMerge'] === '1' || cell['@_hMerge'] === 'true' || cell['@_hMerge'] === true
998
+
999
+ if (isVMerged || isHMerged) {
1000
+ continue
1001
+ }
1002
+
1003
+ // Determine colSpan
1004
+ let colSpan = 1
1005
+ if (cell['@_gridSpan'] !== undefined) {
1006
+ colSpan = parseInt(cell['@_gridSpan'], 10)
1007
+ } else {
1008
+ let nextCol = c + 1
1009
+ while (nextCol < numCols) {
1010
+ const nextCell = tcs[nextCol]
1011
+ if (
1012
+ nextCell &&
1013
+ (nextCell['@_hMerge'] === '1' ||
1014
+ nextCell['@_hMerge'] === 'true' ||
1015
+ nextCell['@_hMerge'] === true)
1016
+ ) {
1017
+ colSpan++
1018
+ nextCol++
1019
+ } else {
1020
+ break
1021
+ }
1022
+ }
1023
+ }
728
1024
 
729
- if (gridSpan > 1 || rowSpan > 1) {
1025
+ // Determine rowSpan
1026
+ let rowSpan = 1
1027
+ if (cell['@_rowSpan'] !== undefined) {
1028
+ rowSpan = parseInt(cell['@_rowSpan'], 10)
1029
+ } else {
1030
+ let nextRow = r + 1
1031
+ while (nextRow < numRows) {
1032
+ const nextCell = trs[nextRow]['a:tc']?.[c]
1033
+ if (
1034
+ nextCell &&
1035
+ (nextCell['@_vMerge'] === '1' ||
1036
+ nextCell['@_vMerge'] === 'true' ||
1037
+ nextCell['@_vMerge'] === true)
1038
+ ) {
1039
+ rowSpan++
1040
+ nextRow++
1041
+ } else {
1042
+ break
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ if (colSpan > 1 || rowSpan > 1) {
730
1048
  merged.push({
731
1049
  startRow: r,
732
1050
  startCol: c,
733
1051
  endRow: r + rowSpan - 1,
734
- endCol: c + gridSpan - 1,
1052
+ endCol: c + colSpan - 1,
735
1053
  })
1054
+
1055
+ for (let i = r; i < r + rowSpan; i++) {
1056
+ for (let j = c; j < c + colSpan; j++) {
1057
+ if (i < numRows && j < numCols) {
1058
+ visited[i][j] = true
1059
+ }
1060
+ }
1061
+ }
736
1062
  }
737
1063
  }
738
1064
  }
@@ -804,12 +1130,20 @@ class TableManager {
804
1130
  const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
805
1131
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
806
1132
 
807
- const trsArr = tblObj['a:tr'] || []
808
1133
  const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
809
1134
 
810
- const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
811
- const pr = parent.row
812
- const pc = parent.col
1135
+ const R = this.getMergeRegion(slideIndex, tableId, rowIndex, colIndex, slideManager)
1136
+ let pr = rowIndex
1137
+ let pc = colIndex
1138
+ let gridSpan = 1
1139
+ let rowSpan = 1
1140
+
1141
+ if (R) {
1142
+ pr = R.startRow
1143
+ pc = R.startCol
1144
+ gridSpan = R.endCol - R.startCol + 1
1145
+ rowSpan = R.endRow - R.startRow + 1
1146
+ }
813
1147
 
814
1148
  let cellLeft = tableX
815
1149
  for (let idx = 0; idx < pc; idx++) {
@@ -821,10 +1155,6 @@ class TableManager {
821
1155
  cellTop += rowHeights[idx] || 0
822
1156
  }
823
1157
 
824
- const parentCell = trsArr[pr]?.['a:tc']?.[pc]
825
- const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
826
- const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
827
-
828
1158
  let cellWidth = 0
829
1159
  for (let idx = 0; idx < gridSpan; idx++) {
830
1160
  cellWidth += colWidths[pc + idx] || 0
@@ -1389,135 +1719,119 @@ class TableManager {
1389
1719
  }
1390
1720
 
1391
1721
  // 2. Determine alignment settings
1392
- let alignX = config.alignX || config.horizontal
1393
- let alignY = config.alignY || config.vertical
1394
-
1395
- if (alignX) {
1396
- alignX = String(alignX).toLowerCase()
1397
- if (alignX === 'middle') alignX = 'center'
1398
- }
1399
- if (alignY) {
1400
- alignY = String(alignY).toLowerCase()
1401
- if (alignY === 'center') alignY = 'middle'
1402
- }
1403
-
1404
- if (config.position) {
1405
- switch (config.position) {
1406
- case 'top-left':
1407
- if (!alignX) alignX = 'left'
1408
- if (!alignY) alignY = 'top'
1409
- break
1410
- case 'top-center':
1411
- case 'top':
1412
- if (!alignX) alignX = 'center'
1413
- if (!alignY) alignY = 'top'
1414
- break
1415
- case 'top-right':
1416
- if (!alignX) alignX = 'right'
1417
- if (!alignY) alignY = 'top'
1418
- break
1419
- case 'middle-left':
1420
- case 'left':
1421
- if (!alignX) alignX = 'left'
1422
- if (!alignY) alignY = 'middle'
1423
- break
1424
- case 'center':
1425
- case 'middle-center':
1426
- if (!alignX) alignX = 'center'
1427
- if (!alignY) alignY = 'middle'
1428
- break
1429
- case 'middle-right':
1430
- case 'right':
1431
- if (!alignX) alignX = 'right'
1432
- if (!alignY) alignY = 'middle'
1433
- break
1434
- case 'bottom-left':
1435
- if (!alignX) alignX = 'left'
1436
- if (!alignY) alignY = 'bottom'
1437
- break
1438
- case 'bottom-center':
1439
- case 'bottom':
1440
- if (!alignX) alignX = 'center'
1441
- if (!alignY) alignY = 'bottom'
1442
- break
1443
- case 'bottom-right':
1444
- if (!alignX) alignX = 'right'
1445
- if (!alignY) alignY = 'bottom'
1446
- break
1722
+ let alignX = undefined
1723
+ let alignY = undefined
1724
+
1725
+ const pos = String(config.position || '')
1726
+ .toLowerCase()
1727
+ .trim()
1728
+ if (pos) {
1729
+ if (pos === 'left') {
1730
+ alignX = 'left'
1731
+ alignY = 'middle'
1732
+ } else if (pos === 'right') {
1733
+ alignX = 'right'
1734
+ alignY = 'middle'
1735
+ } else if (pos === 'center' || pos === 'middle') {
1736
+ alignX = 'center'
1737
+ alignY = 'middle'
1738
+ } else if (pos === 'top') {
1739
+ alignX = 'center'
1740
+ alignY = 'top'
1741
+ } else if (pos === 'bottom') {
1742
+ alignX = 'center'
1743
+ alignY = 'bottom'
1744
+ } else if (pos === 'top-left') {
1745
+ alignX = 'left'
1746
+ alignY = 'top'
1747
+ } else if (pos === 'top-right') {
1748
+ alignX = 'right'
1749
+ alignY = 'top'
1750
+ } else if (pos === 'bottom-left') {
1751
+ alignX = 'left'
1752
+ alignY = 'bottom'
1753
+ } else if (pos === 'bottom-right') {
1754
+ alignX = 'right'
1755
+ alignY = 'bottom'
1756
+ }
1757
+ }
1758
+
1759
+ if (!alignX) {
1760
+ const ax = config.alignX || config.horizontal
1761
+ if (ax) {
1762
+ alignX = String(ax).toLowerCase().trim()
1763
+ if (alignX === 'middle') alignX = 'center'
1764
+ }
1765
+ }
1766
+ if (!alignY) {
1767
+ const ay = config.alignY || config.vertical
1768
+ if (ay) {
1769
+ alignY = String(ay).toLowerCase().trim()
1770
+ if (alignY === 'center') alignY = 'middle'
1771
+ }
1772
+ }
1773
+
1774
+ if (!alignX && !alignY) {
1775
+ if (config.x !== undefined || config.y !== undefined) {
1776
+ alignX = 'left'
1777
+ alignY = 'top'
1778
+ } else {
1779
+ alignX = 'center'
1780
+ alignY = 'middle'
1447
1781
  }
1448
- }
1449
-
1450
- if (alignX && !alignY && config.y === undefined) {
1782
+ } else if (alignX && !alignY) {
1451
1783
  alignY = 'middle'
1452
- }
1453
- if (alignY && !alignX && config.x === undefined) {
1784
+ } else if (alignY && !alignX) {
1454
1785
  alignX = 'center'
1455
1786
  }
1456
1787
 
1457
- if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
1458
- alignX = 'center'
1459
- alignY = 'middle'
1460
- }
1461
-
1462
1788
  // 3. Compute coordinates
1463
1789
  let shapeLeft = cellLeft_px
1464
1790
  let shapeTop = cellTop_px
1465
1791
 
1466
1792
  if (isCellAnchored) {
1793
+ let dx = 0
1794
+ const hasOffsetValX =
1795
+ config.offsetX !== undefined || config.xOffset !== undefined || config.x !== undefined
1796
+ if (config.offsetX !== undefined) dx = parseFloat(config.offsetX)
1797
+ else if (config.xOffset !== undefined) dx = parseFloat(config.xOffset)
1798
+ else if (config.x !== undefined) dx = parseFloat(config.x)
1799
+
1800
+ let dy = 0
1801
+ const hasOffsetValY =
1802
+ config.offsetY !== undefined || config.yOffset !== undefined || config.y !== undefined
1803
+ if (config.offsetY !== undefined) dy = parseFloat(config.offsetY)
1804
+ else if (config.yOffset !== undefined) dy = parseFloat(config.yOffset)
1805
+ else if (config.y !== undefined) dy = parseFloat(config.y)
1806
+
1807
+ shapeLeft = cellLeft_px
1467
1808
  if (alignX === 'left') {
1468
- shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
1809
+ const padding = hasOffsetValX ? dx : 5
1810
+ shapeLeft = cellLeft_px + padding
1469
1811
  } else if (alignX === 'center') {
1470
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
1812
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + dx
1471
1813
  } else if (alignX === 'right') {
1472
- shapeLeft =
1473
- cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
1474
- } else {
1475
- shapeLeft =
1476
- cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
1814
+ const padding = hasOffsetValX ? dx : 5
1815
+ shapeLeft = cellLeft_px + cellWidth_px - shapeWidth - padding
1477
1816
  }
1478
1817
 
1818
+ shapeTop = cellTop_px
1479
1819
  if (alignY === 'top') {
1480
- shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
1820
+ const padding = hasOffsetValY ? dy : 5
1821
+ shapeTop = cellTop_px + padding
1481
1822
  } else if (alignY === 'middle') {
1482
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
1823
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + dy
1483
1824
  } else if (alignY === 'bottom') {
1484
- shapeTop =
1485
- cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
1486
- } else {
1487
- shapeTop =
1488
- cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
1825
+ const padding = hasOffsetValY ? dy : 5
1826
+ shapeTop = cellTop_px + cellHeight_px - shapeHeight - padding
1489
1827
  }
1490
1828
 
1491
1829
  // 4. Boundary Constraints Validation/Enforcement
1492
- if (shapeWidth > cellWidth_px) {
1493
- if (alignX === 'center') {
1494
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1495
- } else if (alignX === 'right') {
1496
- shapeLeft = cellLeft_px + cellWidth_px - shapeWidth
1497
- } else {
1498
- shapeLeft = cellLeft_px
1499
- }
1500
- } else {
1501
- shapeLeft = Math.max(
1502
- cellLeft_px,
1503
- Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1504
- )
1505
- }
1506
-
1507
- if (shapeHeight > cellHeight_px) {
1508
- if (alignY === 'middle') {
1509
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1510
- } else if (alignY === 'bottom') {
1511
- shapeTop = cellTop_px + cellHeight_px - shapeHeight
1512
- } else {
1513
- shapeTop = cellTop_px
1514
- }
1515
- } else {
1516
- shapeTop = Math.max(
1517
- cellTop_px,
1518
- Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
1519
- )
1520
- }
1830
+ shapeLeft = Math.max(
1831
+ cellLeft_px,
1832
+ Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1833
+ )
1834
+ shapeTop = Math.max(cellTop_px, Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight))
1521
1835
  } else {
1522
1836
  shapeLeft = config.x || 0
1523
1837
  shapeTop = config.y || 0
@@ -1920,6 +2234,18 @@ class TableManager {
1920
2234
  expandedConfig.id = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${finalShapeIndex}`
1921
2235
 
1922
2236
  shapeManager.addShape(slideIndex, expandedConfig, slideManager)
2237
+
2238
+ // Register this shape's original config so it can be repositioned after
2239
+ // any subsequent table mutations (row removal, insertion, merge, etc.)
2240
+ this.#cellShapeAnchors.set(expandedConfig.id, {
2241
+ slideIndex,
2242
+ resolvedTableId,
2243
+ tableId,
2244
+ rowIndex,
2245
+ colIndex,
2246
+ shapeIndex: finalShapeIndex,
2247
+ config: { ...options },
2248
+ })
1923
2249
  })
1924
2250
  }
1925
2251
 
@@ -1979,6 +2305,8 @@ class TableManager {
1979
2305
 
1980
2306
  for (const s of matchingShapes) {
1981
2307
  shapeManager.deleteShape(slideIndex, s.name, slideManager)
2308
+ // Deregister from anchor registry
2309
+ this.#cellShapeAnchors.delete(s.name)
1982
2310
  }
1983
2311
  }
1984
2312
 
@@ -1996,6 +2324,188 @@ class TableManager {
1996
2324
  return shapeManager.getShape(slideIndex, primaryShape.name, slideManager)
1997
2325
  }
1998
2326
 
2327
+ /**
2328
+ * Adjusts all registered cell shapes for a table after a row is removed (delta=-1)
2329
+ * or inserted (delta=+1) at `pivotRowIndex`.
2330
+ *
2331
+ * - For delta=-1 and shapes at pivotRowIndex: the shape is deleted (its row is gone).
2332
+ * - For shapes at rows that shifted: delete the old shape and re-add it at the new
2333
+ * row index so that `getCellBounds` can compute correct coordinates from the
2334
+ * updated table layout.
2335
+ *
2336
+ * @private
2337
+ */
2338
+ #adjustCellShapesAfterRowShift(
2339
+ slideIndex,
2340
+ resolvedTableId,
2341
+ tableId,
2342
+ pivotRowIndex,
2343
+ delta,
2344
+ slideManager,
2345
+ shapeManager
2346
+ ) {
2347
+ if (!shapeManager) return
2348
+
2349
+ // Collect entries first (avoid mutating map while iterating)
2350
+ const toDelete = []
2351
+ const toReindex = []
2352
+
2353
+ for (const [name, anchor] of this.#cellShapeAnchors) {
2354
+ if (anchor.slideIndex !== slideIndex || anchor.resolvedTableId !== resolvedTableId) continue
2355
+
2356
+ if (delta < 0 && anchor.rowIndex === pivotRowIndex) {
2357
+ // Row was removed — delete the shape
2358
+ toDelete.push(name)
2359
+ } else if (delta < 0 && anchor.rowIndex > pivotRowIndex) {
2360
+ // Row shifted up (removal below pivot)
2361
+ toReindex.push({ name, anchor, newRowIndex: anchor.rowIndex + delta })
2362
+ } else if (delta > 0 && anchor.rowIndex >= pivotRowIndex) {
2363
+ // Row shifted down (insertion at or above this row)
2364
+ toReindex.push({ name, anchor, newRowIndex: anchor.rowIndex + delta })
2365
+ }
2366
+ }
2367
+
2368
+ // Delete shapes for the removed row
2369
+ for (const name of toDelete) {
2370
+ try {
2371
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2372
+ } catch (e) {
2373
+ logger.warn(`Failed to delete cell shape "${name}": ${e.message}`)
2374
+ }
2375
+ this.#cellShapeAnchors.delete(name)
2376
+ }
2377
+
2378
+ // Sort toReindex: first by newRowIndex, then by colIndex, then by shapeIndex (base index)
2379
+ toReindex.sort((a, b) => {
2380
+ if (a.newRowIndex !== b.newRowIndex) {
2381
+ return a.newRowIndex - b.newRowIndex
2382
+ }
2383
+ if (a.anchor.colIndex !== b.anchor.colIndex) {
2384
+ return a.anchor.colIndex - b.anchor.colIndex
2385
+ }
2386
+ const aBase =
2387
+ typeof a.anchor.shapeIndex === 'string'
2388
+ ? parseInt(a.anchor.shapeIndex.split('_')[0], 10)
2389
+ : a.anchor.shapeIndex
2390
+ const bBase =
2391
+ typeof b.anchor.shapeIndex === 'string'
2392
+ ? parseInt(b.anchor.shapeIndex.split('_')[0], 10)
2393
+ : b.anchor.shapeIndex
2394
+ return aBase - bBase
2395
+ })
2396
+
2397
+ // Phase 1: delete all old shapes to prevent collisions when re-adding
2398
+ for (const { name } of toReindex) {
2399
+ try {
2400
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2401
+ } catch (e) {
2402
+ logger.warn(`Failed to delete cell shape "${name}" during reindex: ${e.message}`)
2403
+ }
2404
+ this.#cellShapeAnchors.delete(name)
2405
+ }
2406
+
2407
+ // Phase 2: re-add all shapes at their newRowIndex
2408
+ for (const { anchor, newRowIndex } of toReindex) {
2409
+ try {
2410
+ this.addCellShape(
2411
+ slideIndex,
2412
+ anchor.tableId,
2413
+ newRowIndex,
2414
+ anchor.colIndex,
2415
+ anchor.config,
2416
+ slideManager,
2417
+ shapeManager
2418
+ )
2419
+ } catch (e) {
2420
+ logger.warn(
2421
+ `Failed to re-add cell shape for (${newRowIndex}, ${anchor.colIndex}): ${e.message}`
2422
+ )
2423
+ }
2424
+ }
2425
+ }
2426
+
2427
+ /**
2428
+ * Repositions all registered cell shapes that fall within a table region
2429
+ * (e.g. after a merge or unmerge). Shapes in the region are deleted and
2430
+ * re-added targeting `(startRow, startCol)` so their coordinates are
2431
+ * recomputed against the merged cell's full bounding box.
2432
+ *
2433
+ * @private
2434
+ */
2435
+ #repositionCellShapesInRegion(
2436
+ slideIndex,
2437
+ tableId,
2438
+ resolvedTableId,
2439
+ startRow,
2440
+ startCol,
2441
+ endRow,
2442
+ endCol,
2443
+ slideManager,
2444
+ shapeManager
2445
+ ) {
2446
+ if (!shapeManager) return
2447
+
2448
+ const toReposition = []
2449
+
2450
+ for (const [name, anchor] of this.#cellShapeAnchors) {
2451
+ if (anchor.slideIndex !== slideIndex || anchor.resolvedTableId !== resolvedTableId) continue
2452
+ if (
2453
+ anchor.rowIndex >= startRow &&
2454
+ anchor.rowIndex <= endRow &&
2455
+ anchor.colIndex >= startCol &&
2456
+ anchor.colIndex <= endCol
2457
+ ) {
2458
+ toReposition.push({ name, anchor })
2459
+ }
2460
+ }
2461
+
2462
+ // Sort toReposition: first by original rowIndex, then by colIndex, then by shapeIndex (base index)
2463
+ toReposition.sort((a, b) => {
2464
+ if (a.anchor.rowIndex !== b.anchor.rowIndex) {
2465
+ return a.anchor.rowIndex - b.anchor.rowIndex
2466
+ }
2467
+ if (a.anchor.colIndex !== b.anchor.colIndex) {
2468
+ return a.anchor.colIndex - b.anchor.colIndex
2469
+ }
2470
+ const aBase =
2471
+ typeof a.anchor.shapeIndex === 'string'
2472
+ ? parseInt(a.anchor.shapeIndex.split('_')[0], 10)
2473
+ : a.anchor.shapeIndex
2474
+ const bBase =
2475
+ typeof b.anchor.shapeIndex === 'string'
2476
+ ? parseInt(b.anchor.shapeIndex.split('_')[0], 10)
2477
+ : b.anchor.shapeIndex
2478
+ return aBase - bBase
2479
+ })
2480
+
2481
+ // Phase 1: delete all old shapes first
2482
+ for (const { name } of toReposition) {
2483
+ try {
2484
+ shapeManager.deleteShape(slideIndex, name, slideManager)
2485
+ } catch (e) {
2486
+ logger.warn(`Failed to delete cell shape "${name}" during reposition: ${e.message}`)
2487
+ }
2488
+ this.#cellShapeAnchors.delete(name)
2489
+ }
2490
+
2491
+ // Phase 2: re-add all shapes targeting startRow, startCol
2492
+ for (const { anchor } of toReposition) {
2493
+ try {
2494
+ this.addCellShape(
2495
+ slideIndex,
2496
+ anchor.tableId,
2497
+ startRow,
2498
+ startCol,
2499
+ anchor.config,
2500
+ slideManager,
2501
+ shapeManager
2502
+ )
2503
+ } catch (e) {
2504
+ logger.warn(`Failed to reposition cell shape for (${startRow}, ${startCol}): ${e.message}`)
2505
+ }
2506
+ }
2507
+ }
2508
+
1999
2509
  /**
2000
2510
  * Generates a new rowId for the given row object.
2001
2511
  */