@productcloudos/editor 1.0.5 → 1.0.6

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.
@@ -7737,6 +7737,9 @@ class TableObject extends BaseEmbeddedObject {
7737
7737
  result.mergedCell.markReflowDirty();
7738
7738
  }
7739
7739
  this.clearSelection();
7740
+ // Focus the anchor (top-left) cell of the merged range
7741
+ const normalized = TableCellMerger.normalizeRange(mergeRange);
7742
+ this.focusCell(normalized.start.row, normalized.start.col);
7740
7743
  this.emit('cells-merged', { range: mergeRange });
7741
7744
  this.emit('content-changed', {});
7742
7745
  }
@@ -14293,7 +14296,8 @@ class CanvasManager extends EventEmitter {
14293
14296
  const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
14294
14297
  // Get the slice for this page (for multi-page tables)
14295
14298
  const slice = table.getRenderedSlice(pageIndex);
14296
- const tablePosition = slice?.position || table.renderedPosition;
14299
+ const tablePosition = slice?.position ||
14300
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
14297
14301
  const sliceHeight = slice?.height || table.height;
14298
14302
  // Check if point is within the table slice on this page
14299
14303
  const isInsideTable = tablePosition &&
@@ -14326,6 +14330,7 @@ class CanvasManager extends EventEmitter {
14326
14330
  end: cellAddr
14327
14331
  });
14328
14332
  this.render();
14333
+ this.emit('table-cell-selection-changed', { table });
14329
14334
  e.preventDefault();
14330
14335
  return;
14331
14336
  }
@@ -14605,7 +14610,8 @@ class CanvasManager extends EventEmitter {
14605
14610
  const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14606
14611
  // Get the slice for the current page (for multi-page tables)
14607
14612
  const slice = table.getRenderedSlice(currentPageIndex);
14608
- const tablePosition = slice?.position || table.renderedPosition;
14613
+ const tablePosition = slice?.position ||
14614
+ (table.renderedPageIndex === currentPageIndex ? table.renderedPosition : null);
14609
14615
  const sliceHeight = slice?.height || table.height;
14610
14616
  if (tablePosition) {
14611
14617
  // Check if point is within the table slice on this page
@@ -14635,6 +14641,7 @@ class CanvasManager extends EventEmitter {
14635
14641
  end: cellAddr
14636
14642
  });
14637
14643
  this.render();
14644
+ this.emit('table-cell-selection-changed', { table });
14638
14645
  }
14639
14646
  }
14640
14647
  }
@@ -15178,40 +15185,46 @@ class CanvasManager extends EventEmitter {
15178
15185
  canvas.style.cursor = 'move';
15179
15186
  return;
15180
15187
  }
15181
- // Show text cursor for text boxes
15182
- if (object instanceof TextBoxObject) {
15183
- canvas.style.cursor = 'text';
15184
- return;
15188
+ // Show text cursor for objects in edit mode, arrow otherwise
15189
+ if (object instanceof TextBoxObject && this.editingTextBox === object) {
15190
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15191
+ }
15192
+ else if (object instanceof TableObject && this._focusedControl === object) {
15193
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15185
15194
  }
15195
+ else {
15196
+ canvas.style.cursor = 'default';
15197
+ }
15198
+ return;
15186
15199
  }
15187
15200
  }
15188
15201
  // Check for table cells (show text cursor)
15189
15202
  const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15190
15203
  if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15191
- canvas.style.cursor = 'text';
15204
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15192
15205
  return;
15193
15206
  }
15194
15207
  // Check for text regions (body, header, footer - show text cursor)
15195
15208
  const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15196
15209
  if (textRegionHit && textRegionHit.data.type === 'text-region') {
15197
- canvas.style.cursor = 'text';
15210
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15198
15211
  return;
15199
15212
  }
15200
15213
  // Also check if point is within any editable region (body, header, footer)
15201
15214
  // This catches cases where text region hit targets may not cover empty space
15202
15215
  const bodyRegion = this.regionManager.getBodyRegion();
15203
15216
  if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15204
- canvas.style.cursor = 'text';
15217
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15205
15218
  return;
15206
15219
  }
15207
15220
  const headerRegion = this.regionManager.getHeaderRegion();
15208
15221
  if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15209
- canvas.style.cursor = 'text';
15222
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15210
15223
  return;
15211
15224
  }
15212
15225
  const footerRegion = this.regionManager.getFooterRegion();
15213
15226
  if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15214
- canvas.style.cursor = 'text';
15227
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15215
15228
  return;
15216
15229
  }
15217
15230
  canvas.style.cursor = 'default';
@@ -15229,7 +15242,8 @@ class CanvasManager extends EventEmitter {
15229
15242
  const { table, dividerType, index } = target.data;
15230
15243
  // Get the table position from slice info
15231
15244
  const slice = table.getRenderedSlice(pageIndex);
15232
- const tablePosition = slice?.position || table.renderedPosition;
15245
+ const tablePosition = slice?.position ||
15246
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
15233
15247
  if (tablePosition) {
15234
15248
  // Calculate the divider position based on type and index
15235
15249
  let position;
@@ -15957,7 +15971,9 @@ class CanvasManager extends EventEmitter {
15957
15971
  if (obj instanceof TableObject) {
15958
15972
  // For multi-page tables, check if this page has a rendered slice
15959
15973
  const slice = obj.getRenderedSlice(pageIndex);
15960
- const tablePosition = slice?.position || obj.renderedPosition;
15974
+ // Only use renderedPosition if the table was actually rendered on this page
15975
+ const tablePosition = slice?.position ||
15976
+ (obj.renderedPageIndex === pageIndex ? obj.renderedPosition : null);
15961
15977
  if (tablePosition) {
15962
15978
  // Check if point is inside the table slice on this page
15963
15979
  const sliceHeight = slice?.height || obj.height;
@@ -16201,6 +16217,10 @@ class CanvasManager extends EventEmitter {
16201
16217
  }
16202
16218
  CanvasManager.CELL_SELECTION_THRESHOLD = 5; // Minimum pixels to drag before cell selection starts
16203
16219
  CanvasManager.RELATIVE_DRAG_THRESHOLD = 3; // Minimum pixels to drag before moving starts
16220
+ // Custom text cursor as a black I-beam SVG data URI.
16221
+ // The native 'text' cursor can render as white on Windows browsers,
16222
+ // making it invisible over the white canvas background.
16223
+ CanvasManager.TEXT_CURSOR = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='20' viewBox='0 0 16 20'%3E%3Cpath d='M5 1h2v1h2V1h2v2h-2v6h2v2h-2v6h2v2h-2v-1H7v1H5v-2h2v-6H5V9h2V3H5z' fill='%23000'/%3E%3C/svg%3E\") 8 10, text";
16204
16224
 
16205
16225
  /**
16206
16226
  * DataBinder handles binding data to documents.
@@ -21489,6 +21509,10 @@ class PCEditor extends EventEmitter {
21489
21509
  this.canvasManager.on('tablecell-cursor-changed', (data) => {
21490
21510
  this.emit('tablecell-cursor-changed', data);
21491
21511
  });
21512
+ // Forward table cell selection changes (multi-cell drag/shift-click)
21513
+ this.canvasManager.on('table-cell-selection-changed', (data) => {
21514
+ this.emit('table-cell-selection-changed', data);
21515
+ });
21492
21516
  this.canvasManager.on('repeating-section-clicked', (data) => {
21493
21517
  // Repeating section clicked - update selection state
21494
21518
  if (data.section && data.section.id) {
@@ -23458,6 +23482,39 @@ class PCEditor extends EventEmitter {
23458
23482
  table.removeColumn(colIndex);
23459
23483
  this.canvasManager.render();
23460
23484
  }
23485
+ /**
23486
+ * Merge selected cells in a table.
23487
+ * Uses the table's current cell selection range.
23488
+ * @param table The table containing the cells to merge
23489
+ * @returns true if cells were merged successfully
23490
+ */
23491
+ tableMergeCells(table) {
23492
+ Logger.log('[pc-editor] tableMergeCells');
23493
+ if (!this._isReady)
23494
+ return false;
23495
+ const result = table.mergeCells();
23496
+ if (result.success) {
23497
+ this.canvasManager.render();
23498
+ }
23499
+ return result.success;
23500
+ }
23501
+ /**
23502
+ * Split a merged cell back into individual cells.
23503
+ * @param table The table containing the merged cell
23504
+ * @param row Row index of the merged cell
23505
+ * @param col Column index of the merged cell
23506
+ * @returns true if the cell was split successfully
23507
+ */
23508
+ tableSplitCell(table, row, col) {
23509
+ Logger.log('[pc-editor] tableSplitCell', row, col);
23510
+ if (!this._isReady)
23511
+ return false;
23512
+ const result = table.splitCell(row, col);
23513
+ if (result.success) {
23514
+ this.canvasManager.render();
23515
+ }
23516
+ return result.success;
23517
+ }
23461
23518
  /**
23462
23519
  * Begin a compound operation. Groups multiple mutations into a single undo entry.
23463
23520
  * Call endCompoundOperation after making changes.
@@ -26500,9 +26557,15 @@ class FormattingPane extends BasePane {
26500
26557
  this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26501
26558
  this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26502
26559
  }
26560
+ else {
26561
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
26562
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
26563
+ }
26503
26564
  }
26504
26565
  catch {
26505
26566
  // No text editing active
26567
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
26568
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
26506
26569
  }
26507
26570
  }
26508
26571
  getSelection() {
@@ -27817,8 +27880,9 @@ class TablePane extends BasePane {
27817
27880
  // Default controls
27818
27881
  this.defaultPaddingInput = null;
27819
27882
  this.defaultBorderColorInput = null;
27820
- // Row loop controls
27821
- this.loopFieldInput = null;
27883
+ // Merge/split buttons
27884
+ this.mergeCellsBtn = null;
27885
+ this.splitCellBtn = null;
27822
27886
  // Cell formatting controls
27823
27887
  this.cellBgColorInput = null;
27824
27888
  this.borderTopCheck = null;
@@ -27838,12 +27902,12 @@ class TablePane extends BasePane {
27838
27902
  // Listen for selection/focus changes
27839
27903
  const updateHandler = () => this.updateFromFocusedTable();
27840
27904
  this.editor.on('selection-change', updateHandler);
27841
- this.editor.on('table-cell-focus', updateHandler);
27842
- this.editor.on('table-cell-selection', updateHandler);
27905
+ this.editor.on('tablecell-cursor-changed', updateHandler);
27906
+ this.editor.on('table-cell-selection-changed', updateHandler);
27843
27907
  this.eventCleanup.push(() => {
27844
27908
  this.editor?.off('selection-change', updateHandler);
27845
- this.editor?.off('table-cell-focus', updateHandler);
27846
- this.editor?.off('table-cell-selection', updateHandler);
27909
+ this.editor?.off('tablecell-cursor-changed', updateHandler);
27910
+ this.editor?.off('table-cell-selection-changed', updateHandler);
27847
27911
  });
27848
27912
  // Initial update
27849
27913
  this.updateFromFocusedTable();
@@ -27896,16 +27960,6 @@ class TablePane extends BasePane {
27896
27960
  this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27897
27961
  headersSection.appendChild(applyHeadersBtn);
27898
27962
  container.appendChild(headersSection);
27899
- // Row Loop section
27900
- const loopSection = this.createSection('Row Loop');
27901
- this.loopFieldInput = this.createTextInput({ placeholder: 'items' });
27902
- loopSection.appendChild(this.createFormGroup('Array Field', this.loopFieldInput, {
27903
- hint: 'Creates a loop on the currently focused row'
27904
- }));
27905
- const createLoopBtn = this.createButton('Create Row Loop');
27906
- this.addButtonListener(createLoopBtn, () => this.createRowLoop());
27907
- loopSection.appendChild(createLoopBtn);
27908
- container.appendChild(loopSection);
27909
27963
  // Defaults section
27910
27964
  const defaultsSection = this.createSection('Defaults');
27911
27965
  const defaultsRow = this.createRow();
@@ -27922,6 +27976,17 @@ class TablePane extends BasePane {
27922
27976
  const cellSection = this.createSection('Cell Formatting');
27923
27977
  this.cellSelectionDisplay = this.createHint('No cell selected');
27924
27978
  cellSection.appendChild(this.cellSelectionDisplay);
27979
+ // Merge/Split buttons
27980
+ const mergeBtnGroup = this.createButtonGroup();
27981
+ this.mergeCellsBtn = this.createButton('Merge Cells');
27982
+ this.mergeCellsBtn.disabled = true;
27983
+ this.splitCellBtn = this.createButton('Split Cell');
27984
+ this.splitCellBtn.disabled = true;
27985
+ this.addButtonListener(this.mergeCellsBtn, () => this.doMergeCells());
27986
+ this.addButtonListener(this.splitCellBtn, () => this.doSplitCell());
27987
+ mergeBtnGroup.appendChild(this.mergeCellsBtn);
27988
+ mergeBtnGroup.appendChild(this.splitCellBtn);
27989
+ cellSection.appendChild(mergeBtnGroup);
27925
27990
  // Background
27926
27991
  this.cellBgColorInput = this.createColorInput('#ffffff');
27927
27992
  cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
@@ -28038,6 +28103,15 @@ class TablePane extends BasePane {
28038
28103
  return;
28039
28104
  const focusedCell = table.focusedCell;
28040
28105
  const selectedRange = table.selectedRange;
28106
+ // Update merge/split button states
28107
+ if (this.mergeCellsBtn) {
28108
+ const canMerge = selectedRange ? table.canMergeRange(selectedRange).canMerge : false;
28109
+ this.mergeCellsBtn.disabled = !canMerge;
28110
+ }
28111
+ if (this.splitCellBtn) {
28112
+ const canSplit = focusedCell ? table.canSplitCell(focusedCell.row, focusedCell.col).canSplit : false;
28113
+ this.splitCellBtn.disabled = !canSplit;
28114
+ }
28041
28115
  if (selectedRange) {
28042
28116
  const count = (selectedRange.end.row - selectedRange.start.row + 1) *
28043
28117
  (selectedRange.end.col - selectedRange.start.col + 1);
@@ -28195,20 +28269,20 @@ class TablePane extends BasePane {
28195
28269
  hasTable() {
28196
28270
  return this.currentTable !== null;
28197
28271
  }
28198
- createRowLoop() {
28199
- if (!this.editor || !this.currentTable) {
28200
- this.onApplyCallback?.(false, new Error('No table focused'));
28272
+ doMergeCells() {
28273
+ if (!this.editor || !this.currentTable)
28201
28274
  return;
28202
- }
28203
- const fieldPath = this.loopFieldInput?.value.trim() || '';
28204
- if (!fieldPath) {
28205
- this.onApplyCallback?.(false, new Error('Array field path is required'));
28275
+ this.editor.tableMergeCells(this.currentTable);
28276
+ this.updateFromFocusedTable();
28277
+ }
28278
+ doSplitCell() {
28279
+ if (!this.editor || !this.currentTable)
28206
28280
  return;
28207
- }
28208
- // Uses the unified createRepeatingSection API which detects
28209
- // that a table is focused and creates a row loop on the focused row
28210
- this.editor.createRepeatingSection(0, 0, fieldPath);
28211
- this.onApplyCallback?.(true);
28281
+ const focused = this.currentTable.focusedCell;
28282
+ if (!focused)
28283
+ return;
28284
+ this.editor.tableSplitCell(this.currentTable, focused.row, focused.col);
28285
+ this.updateFromFocusedTable();
28212
28286
  }
28213
28287
  /**
28214
28288
  * Update the pane from current editor state.