@productcloudos/editor 1.0.4 → 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.
Files changed (47) hide show
  1. package/dist/pc-editor.esm.js +588 -226
  2. package/dist/pc-editor.esm.js.map +1 -1
  3. package/dist/pc-editor.js +588 -225
  4. package/dist/pc-editor.js.map +1 -1
  5. package/dist/pc-editor.min.js +1 -1
  6. package/dist/pc-editor.min.js.map +1 -1
  7. package/dist/types/lib/core/PCEditor.d.ts +31 -5
  8. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  9. package/dist/types/lib/import/PDFParser.d.ts.map +1 -1
  10. package/dist/types/lib/index.d.ts +1 -0
  11. package/dist/types/lib/index.d.ts.map +1 -1
  12. package/dist/types/lib/objects/EmbeddedObjectFactory.d.ts.map +1 -1
  13. package/dist/types/lib/objects/ImageObject.d.ts +11 -0
  14. package/dist/types/lib/objects/ImageObject.d.ts.map +1 -1
  15. package/dist/types/lib/objects/TextBoxObject.d.ts +5 -2
  16. package/dist/types/lib/objects/TextBoxObject.d.ts.map +1 -1
  17. package/dist/types/lib/objects/table/TableCell.d.ts.map +1 -1
  18. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  19. package/dist/types/lib/objects/table/TableRow.d.ts.map +1 -1
  20. package/dist/types/lib/objects/table/types.d.ts +15 -15
  21. package/dist/types/lib/objects/table/types.d.ts.map +1 -1
  22. package/dist/types/lib/panes/DocumentInfoPane.d.ts +3 -1
  23. package/dist/types/lib/panes/DocumentInfoPane.d.ts.map +1 -1
  24. package/dist/types/lib/panes/DocumentSettingsPane.d.ts +2 -0
  25. package/dist/types/lib/panes/DocumentSettingsPane.d.ts.map +1 -1
  26. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -1
  27. package/dist/types/lib/panes/HyperlinkPane.d.ts +1 -0
  28. package/dist/types/lib/panes/HyperlinkPane.d.ts.map +1 -1
  29. package/dist/types/lib/panes/ImagePane.d.ts +1 -0
  30. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -1
  31. package/dist/types/lib/panes/TablePane.d.ts +5 -0
  32. package/dist/types/lib/panes/TablePane.d.ts.map +1 -1
  33. package/dist/types/lib/panes/TextBoxPane.d.ts +1 -0
  34. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -1
  35. package/dist/types/lib/rendering/CanvasManager.d.ts +1 -0
  36. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  37. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  38. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  39. package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
  40. package/dist/types/lib/text/TextFormatting.d.ts +12 -1
  41. package/dist/types/lib/text/TextFormatting.d.ts.map +1 -1
  42. package/dist/types/lib/types/index.d.ts +2 -0
  43. package/dist/types/lib/types/index.d.ts.map +1 -1
  44. package/dist/types/lib/undo/transaction/MutationUndo.d.ts.map +1 -1
  45. package/dist/types/lib/utils/logger.d.ts +20 -0
  46. package/dist/types/lib/utils/logger.d.ts.map +1 -0
  47. package/package.json +1 -1
@@ -818,12 +818,13 @@ class TextFormattingManager extends EventEmitter {
818
818
  }
819
819
  /**
820
820
  * Get formatting at a specific character position.
821
- * Returns the position-specific formatting or the default.
821
+ * Returns the position-specific formatting merged with defaults,
822
+ * ensuring all properties are consistently present.
822
823
  */
823
824
  getFormattingAt(position) {
824
825
  const override = this.formatting.get(position);
825
826
  if (override) {
826
- return { ...override };
827
+ return { ...this._defaultFormatting, ...override };
827
828
  }
828
829
  return { ...this._defaultFormatting };
829
830
  }
@@ -910,6 +911,43 @@ class TextFormattingManager extends EventEmitter {
910
911
  getAllFormatting() {
911
912
  return new Map(this.formatting);
912
913
  }
914
+ /**
915
+ * Get formatting as compressed runs for serialization.
916
+ * Only outputs entries where formatting changes from the previous character.
917
+ * Skips leading default formatting to minimize output size.
918
+ * @param textLength Length of the text to serialize formatting for
919
+ */
920
+ getCompressedRuns(textLength) {
921
+ const runs = [];
922
+ const defaultFormat = this._defaultFormatting;
923
+ let lastFormat = null;
924
+ for (let i = 0; i < textLength; i++) {
925
+ const currentFormat = this.getFormattingAt(i);
926
+ const formatChanged = lastFormat === null ||
927
+ currentFormat.fontFamily !== lastFormat.fontFamily ||
928
+ currentFormat.fontSize !== lastFormat.fontSize ||
929
+ currentFormat.fontWeight !== lastFormat.fontWeight ||
930
+ currentFormat.fontStyle !== lastFormat.fontStyle ||
931
+ currentFormat.color !== lastFormat.color ||
932
+ currentFormat.backgroundColor !== lastFormat.backgroundColor;
933
+ if (formatChanged) {
934
+ const isDefault = currentFormat.fontFamily === defaultFormat.fontFamily &&
935
+ currentFormat.fontSize === defaultFormat.fontSize &&
936
+ currentFormat.fontWeight === defaultFormat.fontWeight &&
937
+ currentFormat.fontStyle === defaultFormat.fontStyle &&
938
+ currentFormat.color === defaultFormat.color &&
939
+ currentFormat.backgroundColor === defaultFormat.backgroundColor;
940
+ if (!isDefault || runs.length > 0) {
941
+ runs.push({
942
+ index: i,
943
+ formatting: { ...currentFormat }
944
+ });
945
+ }
946
+ lastFormat = currentFormat;
947
+ }
948
+ }
949
+ return runs;
950
+ }
913
951
  /**
914
952
  * Restore formatting from a map (for deserialization).
915
953
  */
@@ -3813,6 +3851,35 @@ class ImageObject extends BaseEmbeddedObject {
3813
3851
  get hasError() {
3814
3852
  return this._error;
3815
3853
  }
3854
+ /**
3855
+ * Get the loaded HTMLImageElement, if available.
3856
+ * Used by PDFGenerator to convert unsupported formats to PNG.
3857
+ */
3858
+ get imageElement() {
3859
+ return this._loaded ? this._image : null;
3860
+ }
3861
+ /**
3862
+ * Convert the image to a PNG data URL via canvas.
3863
+ * Used when the original format (e.g., SVG, WebP, GIF) is not supported by pdf-lib.
3864
+ * Returns null if the image is not loaded or conversion fails.
3865
+ */
3866
+ toPngDataUrl() {
3867
+ if (!this._loaded || !this._image)
3868
+ return null;
3869
+ try {
3870
+ const canvas = document.createElement('canvas');
3871
+ canvas.width = this._image.naturalWidth || this._size.width;
3872
+ canvas.height = this._image.naturalHeight || this._size.height;
3873
+ const ctx = canvas.getContext('2d');
3874
+ if (!ctx)
3875
+ return null;
3876
+ ctx.drawImage(this._image, 0, 0, canvas.width, canvas.height);
3877
+ return canvas.toDataURL('image/png');
3878
+ }
3879
+ catch {
3880
+ return null;
3881
+ }
3882
+ }
3816
3883
  loadImage() {
3817
3884
  if (!this._src) {
3818
3885
  this._error = true;
@@ -4708,12 +4775,10 @@ class TextBoxObject extends BaseEmbeddedObject {
4708
4775
  this.editing = false;
4709
4776
  }
4710
4777
  toData() {
4711
- // Serialize formatting map to array of [index, style] pairs
4712
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
4713
- const formattingEntries = [];
4714
- formattingMap.forEach((value, key) => {
4715
- formattingEntries.push([key, { ...value }]);
4716
- });
4778
+ // Serialize formatting as compressed runs (only at change boundaries)
4779
+ const text = this._flowingContent.getText();
4780
+ const compressedRuns = this._flowingContent.getFormattingManager().getCompressedRuns(text.length);
4781
+ const formattingEntries = compressedRuns.map(run => [run.index, { ...run.formatting }]);
4717
4782
  // Get substitution fields as array
4718
4783
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
4719
4784
  return {
@@ -4772,12 +4837,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4772
4837
  this._border = { ...boxData.border };
4773
4838
  if (boxData.padding !== undefined)
4774
4839
  this._padding = boxData.padding;
4775
- // Restore formatting runs
4776
- if (boxData.formattingRuns) {
4840
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
4841
+ if (boxData.formattingRuns && boxData.formattingRuns.length > 0) {
4777
4842
  const formattingManager = this._flowingContent.getFormattingManager();
4778
4843
  formattingManager.clear();
4779
- for (const [index, style] of boxData.formattingRuns) {
4780
- formattingManager.applyFormatting(index, index + 1, style);
4844
+ const textLength = this._flowingContent.getText().length;
4845
+ for (let i = 0; i < boxData.formattingRuns.length; i++) {
4846
+ const [startIndex, style] = boxData.formattingRuns[i];
4847
+ const nextIndex = i + 1 < boxData.formattingRuns.length
4848
+ ? boxData.formattingRuns[i + 1][0]
4849
+ : textLength;
4850
+ if (startIndex < nextIndex) {
4851
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
4852
+ }
4781
4853
  }
4782
4854
  }
4783
4855
  // Restore substitution fields
@@ -4927,15 +4999,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4927
4999
  }
4928
5000
  /**
4929
5001
  * Check if a point is within this text box region.
5002
+ * Uses the full object bounds (including padding/border) so that
5003
+ * double-click to enter edit mode works on the entire text box area,
5004
+ * not just the inner text area.
4930
5005
  * @param point Point in canvas coordinates
4931
- * @param pageIndex The page index (ignored for text boxes)
5006
+ * @param _pageIndex The page index (ignored for text boxes)
4932
5007
  */
4933
- containsPointInRegion(point, pageIndex) {
4934
- const bounds = this.getRegionBounds(pageIndex);
4935
- if (!bounds)
5008
+ containsPointInRegion(point, _pageIndex) {
5009
+ if (!this._renderedPosition)
4936
5010
  return false;
4937
- return point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
4938
- point.y >= bounds.y && point.y <= bounds.y + bounds.height;
5011
+ return point.x >= this._renderedPosition.x &&
5012
+ point.x <= this._renderedPosition.x + this._size.width &&
5013
+ point.y >= this._renderedPosition.y &&
5014
+ point.y <= this._renderedPosition.y + this._size.height;
4939
5015
  }
4940
5016
  /**
4941
5017
  * Trigger a reflow of text in this text box.
@@ -5020,6 +5096,41 @@ function getVerticalPadding(padding) {
5020
5096
  return padding.top + padding.bottom;
5021
5097
  }
5022
5098
 
5099
+ /**
5100
+ * Logger - Centralized logging for PC Editor.
5101
+ *
5102
+ * When enabled, logs informational messages to the console.
5103
+ * When disabled, only errors are logged.
5104
+ * Controlled via EditorOptions.enableLogging or Logger.setEnabled().
5105
+ */
5106
+ let _enabled = false;
5107
+ const Logger = {
5108
+ /** Enable or disable logging. When disabled, only errors are logged. */
5109
+ setEnabled(enabled) {
5110
+ _enabled = enabled;
5111
+ },
5112
+ /** Check if logging is enabled. */
5113
+ isEnabled() {
5114
+ return _enabled;
5115
+ },
5116
+ /** Log an informational message. Only outputs when logging is enabled. */
5117
+ log(...args) {
5118
+ if (_enabled) {
5119
+ console.log(...args);
5120
+ }
5121
+ },
5122
+ /** Log a warning. Only outputs when logging is enabled. */
5123
+ warn(...args) {
5124
+ if (_enabled) {
5125
+ console.warn(...args);
5126
+ }
5127
+ },
5128
+ /** Log an error. Always outputs regardless of logging state. */
5129
+ error(...args) {
5130
+ console.error(...args);
5131
+ }
5132
+ };
5133
+
5023
5134
  /**
5024
5135
  * TableCell - A cell within a table that contains editable text.
5025
5136
  * Implements EditableTextRegion for unified text interaction.
@@ -5070,7 +5181,7 @@ class TableCell extends EventEmitter {
5070
5181
  });
5071
5182
  // Prevent embedded objects in table cells (only substitution fields allowed)
5072
5183
  this._flowingContent.insertEmbeddedObject = () => {
5073
- console.warn('Embedded objects are not allowed in table cells. Use insertSubstitutionField instead.');
5184
+ Logger.warn('[pc-editor:TableCell] Embedded objects are not allowed in table cells. Use insertSubstitutionField instead.');
5074
5185
  };
5075
5186
  // Set initial content
5076
5187
  if (config.content) {
@@ -5387,7 +5498,7 @@ class TableCell extends EventEmitter {
5387
5498
  this._reflowDirty = false;
5388
5499
  this._lastReflowWidth = width;
5389
5500
  this._cachedContentHeight = null; // Clear cached height since lines changed
5390
- console.log('[TableCell.reflow] cellId:', this._id, 'text:', JSON.stringify(this._flowingContent.getText()), 'lines:', this._flowedLines.length);
5501
+ Logger.log('[pc-editor:TableCell.reflow] cellId:', this._id, 'text:', JSON.stringify(this._flowingContent.getText()), 'lines:', this._flowedLines.length);
5391
5502
  }
5392
5503
  /**
5393
5504
  * Mark this cell as needing reflow.
@@ -5425,7 +5536,7 @@ class TableCell extends EventEmitter {
5425
5536
  return this._editing && this._flowingContent.hasFocus();
5426
5537
  }
5427
5538
  handleKeyDown(e) {
5428
- console.log('[TableCell.handleKeyDown] Key:', e.key, '_editing:', this._editing, 'flowingContent.hasFocus:', this._flowingContent.hasFocus());
5539
+ Logger.log('[pc-editor:TableCell.handleKeyDown] Key:', e.key, '_editing:', this._editing, 'flowingContent.hasFocus:', this._flowingContent.hasFocus());
5429
5540
  if (!this._editing)
5430
5541
  return false;
5431
5542
  // Let parent table handle Tab navigation
@@ -5433,9 +5544,9 @@ class TableCell extends EventEmitter {
5433
5544
  return false; // Not handled - parent will handle
5434
5545
  }
5435
5546
  // Delegate to FlowingTextContent
5436
- console.log('[TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5547
+ Logger.log('[pc-editor:TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5437
5548
  const handled = this._flowingContent.handleKeyDown(e);
5438
- console.log('[TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5549
+ Logger.log('[pc-editor:TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5439
5550
  return handled;
5440
5551
  }
5441
5552
  onCursorBlink(handler) {
@@ -5507,28 +5618,49 @@ class TableCell extends EventEmitter {
5507
5618
  // Serialization
5508
5619
  // ============================================
5509
5620
  toData() {
5510
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
5511
- const formattingRuns = [];
5512
- formattingMap.forEach((style, index) => {
5513
- formattingRuns.push([index, { ...style }]);
5514
- });
5621
+ const text = this._flowingContent.getText();
5622
+ const compressedRuns = this._flowingContent.getFormattingManager().getCompressedRuns(text.length);
5623
+ const formattingRuns = compressedRuns.map(run => [run.index, run.formatting]);
5515
5624
  // Get substitution fields for serialization
5516
5625
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
5517
- return {
5518
- id: this._id,
5519
- rowSpan: this._rowSpan,
5520
- colSpan: this._colSpan,
5521
- backgroundColor: this._backgroundColor,
5522
- border: this._border,
5523
- padding: this._padding,
5524
- verticalAlign: this._verticalAlign,
5525
- content: this._flowingContent.getText(),
5526
- fontFamily: this._fontFamily,
5527
- fontSize: this._fontSize,
5528
- color: this._color,
5529
- formattingRuns: formattingRuns.length > 0 ? formattingRuns : undefined,
5530
- substitutionFields: fields.length > 0 ? fields : undefined
5531
- };
5626
+ // Only include non-default values to minimize export size
5627
+ const defaults = DEFAULT_TABLE_STYLE;
5628
+ const defaultBorder = DEFAULT_CELL_BORDER_SIDE;
5629
+ const isDefaultBorderSide = (side) => side.width === defaultBorder.width && side.color === defaultBorder.color && side.style === defaultBorder.style;
5630
+ const isDefaultBorder = isDefaultBorderSide(this._border.top) &&
5631
+ isDefaultBorderSide(this._border.right) &&
5632
+ isDefaultBorderSide(this._border.bottom) &&
5633
+ isDefaultBorderSide(this._border.left);
5634
+ const isDefaultPadding = this._padding.top === defaults.cellPadding &&
5635
+ this._padding.right === defaults.cellPadding &&
5636
+ this._padding.bottom === defaults.cellPadding &&
5637
+ this._padding.left === defaults.cellPadding;
5638
+ const data = {};
5639
+ if (this._rowSpan !== 1)
5640
+ data.rowSpan = this._rowSpan;
5641
+ if (this._colSpan !== 1)
5642
+ data.colSpan = this._colSpan;
5643
+ if (this._backgroundColor !== defaults.backgroundColor)
5644
+ data.backgroundColor = this._backgroundColor;
5645
+ if (!isDefaultBorder)
5646
+ data.border = this._border;
5647
+ if (!isDefaultPadding)
5648
+ data.padding = this._padding;
5649
+ if (this._verticalAlign !== 'top')
5650
+ data.verticalAlign = this._verticalAlign;
5651
+ if (text)
5652
+ data.content = text;
5653
+ if (this._fontFamily !== defaults.fontFamily)
5654
+ data.fontFamily = this._fontFamily;
5655
+ if (this._fontSize !== defaults.fontSize)
5656
+ data.fontSize = this._fontSize;
5657
+ if (this._color !== defaults.color)
5658
+ data.color = this._color;
5659
+ if (formattingRuns.length > 0)
5660
+ data.formattingRuns = formattingRuns;
5661
+ if (fields.length > 0)
5662
+ data.substitutionFields = fields;
5663
+ return data;
5532
5664
  }
5533
5665
  static fromData(data) {
5534
5666
  const cell = new TableCell({
@@ -5544,14 +5676,19 @@ class TableCell extends EventEmitter {
5544
5676
  fontSize: data.fontSize,
5545
5677
  color: data.color
5546
5678
  });
5547
- // Restore formatting runs
5548
- if (data.formattingRuns) {
5679
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
5680
+ if (data.formattingRuns && data.formattingRuns.length > 0) {
5549
5681
  const formattingManager = cell._flowingContent.getFormattingManager();
5550
- const formattingMap = new Map();
5551
- for (const [index, style] of data.formattingRuns) {
5552
- formattingMap.set(index, style);
5682
+ const textLength = (data.content || '').length;
5683
+ for (let i = 0; i < data.formattingRuns.length; i++) {
5684
+ const [startIndex, style] = data.formattingRuns[i];
5685
+ const nextIndex = i + 1 < data.formattingRuns.length
5686
+ ? data.formattingRuns[i + 1][0]
5687
+ : textLength;
5688
+ if (startIndex < nextIndex) {
5689
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
5690
+ }
5553
5691
  }
5554
- formattingManager.setAllFormatting(formattingMap);
5555
5692
  }
5556
5693
  // Restore substitution fields
5557
5694
  if (data.substitutionFields && Array.isArray(data.substitutionFields)) {
@@ -5785,13 +5922,17 @@ class TableRow extends EventEmitter {
5785
5922
  // Serialization
5786
5923
  // ============================================
5787
5924
  toData() {
5788
- return {
5789
- id: this._id,
5790
- height: this._height,
5791
- minHeight: this._minHeight,
5792
- isHeader: this._isHeader,
5925
+ const data = {
5793
5926
  cells: this._cells.map(cell => cell.toData())
5794
5927
  };
5928
+ // Only include non-default values
5929
+ if (this._height !== null)
5930
+ data.height = this._height;
5931
+ if (this._minHeight !== DEFAULT_TABLE_STYLE.minRowHeight)
5932
+ data.minHeight = this._minHeight;
5933
+ if (this._isHeader)
5934
+ data.isHeader = this._isHeader;
5935
+ return data;
5795
5936
  }
5796
5937
  static fromData(data) {
5797
5938
  const row = new TableRow({
@@ -6125,7 +6266,7 @@ class TableObject extends BaseEmbeddedObject {
6125
6266
  set position(value) {
6126
6267
  // Tables only support block positioning - ignore any attempt to set other modes
6127
6268
  if (value !== 'block') {
6128
- console.warn(`Tables only support 'block' positioning. Ignoring attempt to set '${value}'.`);
6269
+ Logger.warn(`[pc-editor:TableObject] Tables only support 'block' positioning. Ignoring attempt to set '${value}'.`);
6129
6270
  }
6130
6271
  // Always set to block
6131
6272
  super.position = 'block';
@@ -6664,24 +6805,24 @@ class TableObject extends BaseEmbeddedObject {
6664
6805
  createRowLoop(startRowIndex, endRowIndex, fieldPath) {
6665
6806
  // Validate range
6666
6807
  if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
6667
- console.warn('[TableObject.createRowLoop] Invalid row range');
6808
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Invalid row range');
6668
6809
  return null;
6669
6810
  }
6670
6811
  if (startRowIndex > endRowIndex) {
6671
- console.warn('[TableObject.createRowLoop] Start index must be <= end index');
6812
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Start index must be <= end index');
6672
6813
  return null;
6673
6814
  }
6674
6815
  // Check for overlap with existing loops
6675
6816
  for (const existingLoop of this._rowLoops.values()) {
6676
6817
  if (this.loopRangesOverlap(startRowIndex, endRowIndex, existingLoop.startRowIndex, existingLoop.endRowIndex)) {
6677
- console.warn('[TableObject.createRowLoop] Loop range overlaps with existing loop');
6818
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop range overlaps with existing loop');
6678
6819
  return null;
6679
6820
  }
6680
6821
  }
6681
6822
  // Check that loop rows are not header rows
6682
6823
  for (let i = startRowIndex; i <= endRowIndex; i++) {
6683
6824
  if (this._rows[i]?.isHeader) {
6684
- console.warn('[TableObject.createRowLoop] Loop rows cannot be header rows');
6825
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop rows cannot be header rows');
6685
6826
  return null;
6686
6827
  }
6687
6828
  }
@@ -7487,7 +7628,7 @@ class TableObject extends BaseEmbeddedObject {
7487
7628
  return this._editing;
7488
7629
  }
7489
7630
  handleKeyDown(e) {
7490
- console.log('[TableObject.handleKeyDown] Key:', e.key, '_editing:', this._editing, '_focusedCell:', this._focusedCell);
7631
+ Logger.log('[pc-editor:TableObject.handleKeyDown] Key:', e.key, '_editing:', this._editing, '_focusedCell:', this._focusedCell);
7491
7632
  if (!this._editing)
7492
7633
  return false;
7493
7634
  // Handle Tab navigation
@@ -7596,6 +7737,9 @@ class TableObject extends BaseEmbeddedObject {
7596
7737
  result.mergedCell.markReflowDirty();
7597
7738
  }
7598
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);
7599
7743
  this.emit('cells-merged', { range: mergeRange });
7600
7744
  this.emit('content-changed', {});
7601
7745
  }
@@ -8275,14 +8419,20 @@ class EmbeddedObjectFactory {
8275
8419
  border: data.data.border,
8276
8420
  padding: data.data.padding
8277
8421
  });
8278
- // Restore formatting runs if present
8422
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
8279
8423
  if (data.data.formattingRuns && Array.isArray(data.data.formattingRuns)) {
8280
- const formattingManager = textBox.flowingContent.getFormattingManager();
8281
- const formattingMap = new Map();
8282
- for (const [index, style] of data.data.formattingRuns) {
8283
- formattingMap.set(index, style);
8424
+ const runs = data.data.formattingRuns;
8425
+ if (runs.length > 0) {
8426
+ const formattingManager = textBox.flowingContent.getFormattingManager();
8427
+ const textLength = textBox.flowingContent.getText().length;
8428
+ for (let i = 0; i < runs.length; i++) {
8429
+ const [startIndex, style] = runs[i];
8430
+ const nextIndex = i + 1 < runs.length ? runs[i + 1][0] : textLength;
8431
+ if (startIndex < nextIndex) {
8432
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
8433
+ }
8434
+ }
8284
8435
  }
8285
- formattingManager.setAllFormatting(formattingMap);
8286
8436
  }
8287
8437
  // Restore substitution fields if present
8288
8438
  if (data.data.substitutionFields && Array.isArray(data.data.substitutionFields)) {
@@ -9259,9 +9409,9 @@ class FlowingTextContent extends EventEmitter {
9259
9409
  * @returns true if the event was handled, false otherwise
9260
9410
  */
9261
9411
  handleKeyDown(e) {
9262
- console.log('[FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9412
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9263
9413
  if (!this._hasFocus) {
9264
- console.log('[FlowingTextContent.handleKeyDown] No focus, returning false');
9414
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] No focus, returning false');
9265
9415
  return false;
9266
9416
  }
9267
9417
  switch (e.key) {
@@ -9776,46 +9926,19 @@ class FlowingTextContent extends EventEmitter {
9776
9926
  toData() {
9777
9927
  // Serialize text content
9778
9928
  const text = this.textState.getText();
9779
- // Serialize text formatting as runs - only output when format changes
9780
- // This optimizes document size by not storing redundant formatting entries
9781
- const formattingRuns = [];
9782
- const defaultFormat = this.formatting.defaultFormatting;
9783
- let lastFormat = null;
9784
- for (let i = 0; i < text.length; i++) {
9785
- const currentFormat = this.formatting.getFormattingAt(i);
9786
- // Check if formatting changed from previous character
9787
- const formatChanged = lastFormat === null ||
9788
- currentFormat.fontFamily !== lastFormat.fontFamily ||
9789
- currentFormat.fontSize !== lastFormat.fontSize ||
9790
- currentFormat.fontWeight !== lastFormat.fontWeight ||
9791
- currentFormat.fontStyle !== lastFormat.fontStyle ||
9792
- currentFormat.color !== lastFormat.color ||
9793
- currentFormat.backgroundColor !== lastFormat.backgroundColor;
9794
- if (formatChanged) {
9795
- // Only output if different from default (to further reduce size)
9796
- const isDefault = currentFormat.fontFamily === defaultFormat.fontFamily &&
9797
- currentFormat.fontSize === defaultFormat.fontSize &&
9798
- currentFormat.fontWeight === defaultFormat.fontWeight &&
9799
- currentFormat.fontStyle === defaultFormat.fontStyle &&
9800
- currentFormat.color === defaultFormat.color &&
9801
- currentFormat.backgroundColor === defaultFormat.backgroundColor;
9802
- // Always output first run if it's not default, or output when format changes
9803
- if (!isDefault || formattingRuns.length > 0) {
9804
- formattingRuns.push({
9805
- index: i,
9806
- formatting: {
9807
- fontFamily: currentFormat.fontFamily,
9808
- fontSize: currentFormat.fontSize,
9809
- fontWeight: currentFormat.fontWeight,
9810
- fontStyle: currentFormat.fontStyle,
9811
- color: currentFormat.color,
9812
- backgroundColor: currentFormat.backgroundColor
9813
- }
9814
- });
9815
- }
9816
- lastFormat = currentFormat;
9929
+ // Serialize text formatting as compressed runs (only at change boundaries)
9930
+ const compressedRuns = this.formatting.getCompressedRuns(text.length);
9931
+ const formattingRuns = compressedRuns.map(run => ({
9932
+ index: run.index,
9933
+ formatting: {
9934
+ fontFamily: run.formatting.fontFamily,
9935
+ fontSize: run.formatting.fontSize,
9936
+ fontWeight: run.formatting.fontWeight,
9937
+ fontStyle: run.formatting.fontStyle,
9938
+ color: run.formatting.color,
9939
+ backgroundColor: run.formatting.backgroundColor
9817
9940
  }
9818
- }
9941
+ }));
9819
9942
  // Serialize paragraph formatting
9820
9943
  const paragraphFormatting = this.paragraphFormatting.toJSON();
9821
9944
  // Serialize substitution fields
@@ -9911,7 +10034,7 @@ class FlowingTextContent extends EventEmitter {
9911
10034
  content.getEmbeddedObjectManager().insert(object, ref.textIndex);
9912
10035
  }
9913
10036
  else {
9914
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10037
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9915
10038
  }
9916
10039
  }
9917
10040
  }
@@ -9965,7 +10088,7 @@ class FlowingTextContent extends EventEmitter {
9965
10088
  this.embeddedObjects.insert(object, ref.textIndex);
9966
10089
  }
9967
10090
  else {
9968
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10091
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9969
10092
  }
9970
10093
  }
9971
10094
  }
@@ -12504,7 +12627,7 @@ class FlowingTextRenderer extends EventEmitter {
12504
12627
  updateResizeHandleTargets(selectedObjects) {
12505
12628
  // Clear existing resize handle targets
12506
12629
  this._hitTestManager.clearCategory('resize-handles');
12507
- console.log('[updateResizeHandleTargets] selectedObjects:', selectedObjects.length);
12630
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets selectedObjects:', selectedObjects.length);
12508
12631
  // Register resize handles for each selected object
12509
12632
  for (const object of selectedObjects) {
12510
12633
  if (!object.resizable)
@@ -12517,7 +12640,7 @@ class FlowingTextRenderer extends EventEmitter {
12517
12640
  // For regular objects, use renderedPosition
12518
12641
  const pos = object.renderedPosition;
12519
12642
  const pageIndex = object.renderedPageIndex;
12520
- console.log('[updateResizeHandleTargets] object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12643
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12521
12644
  if (pos && pageIndex >= 0) {
12522
12645
  this.registerObjectResizeHandles(object, pageIndex, pos);
12523
12646
  }
@@ -14173,7 +14296,8 @@ class CanvasManager extends EventEmitter {
14173
14296
  const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
14174
14297
  // Get the slice for this page (for multi-page tables)
14175
14298
  const slice = table.getRenderedSlice(pageIndex);
14176
- const tablePosition = slice?.position || table.renderedPosition;
14299
+ const tablePosition = slice?.position ||
14300
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
14177
14301
  const sliceHeight = slice?.height || table.height;
14178
14302
  // Check if point is within the table slice on this page
14179
14303
  const isInsideTable = tablePosition &&
@@ -14206,6 +14330,7 @@ class CanvasManager extends EventEmitter {
14206
14330
  end: cellAddr
14207
14331
  });
14208
14332
  this.render();
14333
+ this.emit('table-cell-selection-changed', { table });
14209
14334
  e.preventDefault();
14210
14335
  return;
14211
14336
  }
@@ -14485,7 +14610,8 @@ class CanvasManager extends EventEmitter {
14485
14610
  const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14486
14611
  // Get the slice for the current page (for multi-page tables)
14487
14612
  const slice = table.getRenderedSlice(currentPageIndex);
14488
- const tablePosition = slice?.position || table.renderedPosition;
14613
+ const tablePosition = slice?.position ||
14614
+ (table.renderedPageIndex === currentPageIndex ? table.renderedPosition : null);
14489
14615
  const sliceHeight = slice?.height || table.height;
14490
14616
  if (tablePosition) {
14491
14617
  // Check if point is within the table slice on this page
@@ -14515,6 +14641,7 @@ class CanvasManager extends EventEmitter {
14515
14641
  end: cellAddr
14516
14642
  });
14517
14643
  this.render();
14644
+ this.emit('table-cell-selection-changed', { table });
14518
14645
  }
14519
14646
  }
14520
14647
  }
@@ -15058,40 +15185,46 @@ class CanvasManager extends EventEmitter {
15058
15185
  canvas.style.cursor = 'move';
15059
15186
  return;
15060
15187
  }
15061
- // Show text cursor for text boxes
15062
- if (object instanceof TextBoxObject) {
15063
- canvas.style.cursor = 'text';
15064
- 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;
15065
15191
  }
15192
+ else if (object instanceof TableObject && this._focusedControl === object) {
15193
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15194
+ }
15195
+ else {
15196
+ canvas.style.cursor = 'default';
15197
+ }
15198
+ return;
15066
15199
  }
15067
15200
  }
15068
15201
  // Check for table cells (show text cursor)
15069
15202
  const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15070
15203
  if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15071
- canvas.style.cursor = 'text';
15204
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15072
15205
  return;
15073
15206
  }
15074
15207
  // Check for text regions (body, header, footer - show text cursor)
15075
15208
  const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15076
15209
  if (textRegionHit && textRegionHit.data.type === 'text-region') {
15077
- canvas.style.cursor = 'text';
15210
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15078
15211
  return;
15079
15212
  }
15080
15213
  // Also check if point is within any editable region (body, header, footer)
15081
15214
  // This catches cases where text region hit targets may not cover empty space
15082
15215
  const bodyRegion = this.regionManager.getBodyRegion();
15083
15216
  if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15084
- canvas.style.cursor = 'text';
15217
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15085
15218
  return;
15086
15219
  }
15087
15220
  const headerRegion = this.regionManager.getHeaderRegion();
15088
15221
  if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15089
- canvas.style.cursor = 'text';
15222
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15090
15223
  return;
15091
15224
  }
15092
15225
  const footerRegion = this.regionManager.getFooterRegion();
15093
15226
  if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15094
- canvas.style.cursor = 'text';
15227
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15095
15228
  return;
15096
15229
  }
15097
15230
  canvas.style.cursor = 'default';
@@ -15109,7 +15242,8 @@ class CanvasManager extends EventEmitter {
15109
15242
  const { table, dividerType, index } = target.data;
15110
15243
  // Get the table position from slice info
15111
15244
  const slice = table.getRenderedSlice(pageIndex);
15112
- const tablePosition = slice?.position || table.renderedPosition;
15245
+ const tablePosition = slice?.position ||
15246
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
15113
15247
  if (tablePosition) {
15114
15248
  // Calculate the divider position based on type and index
15115
15249
  let position;
@@ -15163,7 +15297,7 @@ class CanvasManager extends EventEmitter {
15163
15297
  this.emit('element-removed', { elementId: objectId });
15164
15298
  }
15165
15299
  selectElement(elementId) {
15166
- console.log('Selecting element:', elementId);
15300
+ Logger.log('[pc-editor:CanvasManager] Selecting element:', elementId);
15167
15301
  this.selectedElements.add(elementId);
15168
15302
  // Update embedded object's selected state
15169
15303
  const flowingContents = [
@@ -15175,12 +15309,12 @@ class CanvasManager extends EventEmitter {
15175
15309
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15176
15310
  for (const [, obj] of embeddedObjects.entries()) {
15177
15311
  if (obj.id === elementId) {
15178
- console.log('Found embedded object to select:', obj.id);
15312
+ Logger.log('[pc-editor:CanvasManager] Found embedded object to select:', obj.id);
15179
15313
  obj.selected = true;
15180
15314
  }
15181
15315
  }
15182
15316
  }
15183
- console.log('Selected elements after selection:', Array.from(this.selectedElements));
15317
+ Logger.log('[pc-editor:CanvasManager] Selected elements after selection:', Array.from(this.selectedElements));
15184
15318
  this.render();
15185
15319
  this.updateResizeHandleHitTargets();
15186
15320
  this.emit('selection-change', { selectedElements: Array.from(this.selectedElements) });
@@ -15230,10 +15364,10 @@ class CanvasManager extends EventEmitter {
15230
15364
  this.flowingTextRenderer.updateResizeHandleTargets(selectedObjects);
15231
15365
  }
15232
15366
  clearSelection() {
15233
- console.log('clearSelection called, current selected elements:', Array.from(this.selectedElements));
15367
+ Logger.log('[pc-editor:CanvasManager] clearSelection called, current selected elements:', Array.from(this.selectedElements));
15234
15368
  // Clear selected state on all embedded objects
15235
15369
  this.selectedElements.forEach(elementId => {
15236
- console.log('Clearing selection for element:', elementId);
15370
+ Logger.log('[pc-editor:CanvasManager] Clearing selection for element:', elementId);
15237
15371
  // Check embedded objects in all flowing content sources (body, header, footer)
15238
15372
  const flowingContents = [
15239
15373
  this.document.bodyFlowingContent,
@@ -15244,7 +15378,7 @@ class CanvasManager extends EventEmitter {
15244
15378
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15245
15379
  for (const [, embeddedObj] of embeddedObjects.entries()) {
15246
15380
  if (embeddedObj.id === elementId) {
15247
- console.log('Clearing selection on embedded object:', elementId);
15381
+ Logger.log('[pc-editor:CanvasManager] Clearing selection on embedded object:', elementId);
15248
15382
  embeddedObj.selected = false;
15249
15383
  }
15250
15384
  }
@@ -15252,7 +15386,7 @@ class CanvasManager extends EventEmitter {
15252
15386
  });
15253
15387
  this.selectedElements.clear();
15254
15388
  this.selectedSectionId = null;
15255
- console.log('About to render after clearing selection...');
15389
+ Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
15256
15390
  this.render();
15257
15391
  this.updateResizeHandleHitTargets();
15258
15392
  this.emit('selection-change', { selectedElements: [] });
@@ -15501,7 +15635,7 @@ class CanvasManager extends EventEmitter {
15501
15635
  });
15502
15636
  // Handle substitution field clicks
15503
15637
  this.flowingTextRenderer.on('substitution-field-clicked', (data) => {
15504
- console.log('[substitution-field-clicked] Field:', data.field?.fieldName, 'Section:', data.section);
15638
+ Logger.log('[pc-editor:CanvasManager] substitution-field-clicked Field:', data.field?.fieldName, 'Section:', data.section);
15505
15639
  // Emit event for external handling (e.g., showing field properties panel)
15506
15640
  this.emit('substitution-field-clicked', data);
15507
15641
  });
@@ -15837,7 +15971,9 @@ class CanvasManager extends EventEmitter {
15837
15971
  if (obj instanceof TableObject) {
15838
15972
  // For multi-page tables, check if this page has a rendered slice
15839
15973
  const slice = obj.getRenderedSlice(pageIndex);
15840
- 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);
15841
15977
  if (tablePosition) {
15842
15978
  // Check if point is inside the table slice on this page
15843
15979
  const sliceHeight = slice?.height || obj.height;
@@ -15976,14 +16112,15 @@ class CanvasManager extends EventEmitter {
15976
16112
  this.editingTextBox = textBox;
15977
16113
  this._editingTextBoxPageId = pageId || null;
15978
16114
  if (textBox) {
15979
- // Use the unified focus system to handle focus/blur and cursor blink
15980
- // This blurs the previous control, hiding its cursor
15981
- this.setFocus(textBox);
15982
16115
  // Clear selection in main flowing content
15983
16116
  this.document.bodyFlowingContent.clearSelection();
15984
- // Select the text box
16117
+ // Select the text box visually (this calls setFocus(null) internally,
16118
+ // so we must set focus to the text box AFTER this call)
15985
16119
  this.clearSelection();
15986
16120
  this.selectInlineElement({ type: 'embedded-object', object: textBox, textIndex: textBox.textIndex });
16121
+ // Now set focus to the text box for editing — must be AFTER selectInlineElement
16122
+ // because selectBaseEmbeddedObject calls setFocus(null) which would undo it
16123
+ this.setFocus(textBox);
15987
16124
  this.emit('textbox-editing-started', { textBox });
15988
16125
  }
15989
16126
  else {
@@ -16080,6 +16217,10 @@ class CanvasManager extends EventEmitter {
16080
16217
  }
16081
16218
  CanvasManager.CELL_SELECTION_THRESHOLD = 5; // Minimum pixels to drag before cell selection starts
16082
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";
16083
16224
 
16084
16225
  /**
16085
16226
  * DataBinder handles binding data to documents.
@@ -16722,7 +16863,15 @@ class PDFGenerator {
16722
16863
  // Check if it's a data URL we can embed
16723
16864
  if (src.startsWith('data:')) {
16724
16865
  try {
16725
- const embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16866
+ let embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16867
+ // If the format isn't directly supported (e.g., SVG, WebP, GIF),
16868
+ // convert to PNG via canvas and try again
16869
+ if (!embeddedImage) {
16870
+ const pngDataUrl = image.toPngDataUrl();
16871
+ if (pngDataUrl) {
16872
+ embeddedImage = await this.embedImageFromDataUrl(pdfDoc, pngDataUrl);
16873
+ }
16874
+ }
16726
16875
  if (embeddedImage) {
16727
16876
  // Calculate draw position/size based on fit mode
16728
16877
  const drawParams = this.calculateImageDrawParams(embeddedImage.width, embeddedImage.height, image.width, image.height, image.fit);
@@ -16745,7 +16894,7 @@ class PDFGenerator {
16745
16894
  }
16746
16895
  }
16747
16896
  catch (e) {
16748
- console.warn('Failed to embed image:', e);
16897
+ Logger.warn('[pc-editor:PDFGenerator] Failed to embed image:', e);
16749
16898
  }
16750
16899
  }
16751
16900
  // Fallback: draw placeholder rectangle for images we can't embed
@@ -18773,7 +18922,7 @@ class MutationUndo {
18773
18922
  this.undoTableStructure(mutation);
18774
18923
  break;
18775
18924
  default:
18776
- console.warn('Unknown mutation type for undo:', mutation.type);
18925
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for undo:', mutation.type);
18777
18926
  }
18778
18927
  }
18779
18928
  /**
@@ -18823,7 +18972,7 @@ class MutationUndo {
18823
18972
  this.redoTableStructure(mutation);
18824
18973
  break;
18825
18974
  default:
18826
- console.warn('Unknown mutation type for redo:', mutation.type);
18975
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for redo:', mutation.type);
18827
18976
  }
18828
18977
  }
18829
18978
  // --- Text Mutations ---
@@ -20155,13 +20304,13 @@ class PDFParser {
20155
20304
  }
20156
20305
  catch {
20157
20306
  // Skip images that fail to extract
20158
- console.warn(`Failed to extract image: ${imageName}`);
20307
+ Logger.warn(`[pc-editor:PDFParser] Failed to extract image: ${imageName}`);
20159
20308
  }
20160
20309
  }
20161
20310
  }
20162
20311
  }
20163
20312
  catch (error) {
20164
- console.warn('Image extraction failed:', error);
20313
+ Logger.warn('[pc-editor:PDFParser] Image extraction failed:', error);
20165
20314
  }
20166
20315
  return images;
20167
20316
  }
@@ -21188,6 +21337,8 @@ class PCEditor extends EventEmitter {
21188
21337
  }
21189
21338
  this.container = container;
21190
21339
  this.options = this.mergeOptions(options);
21340
+ // Initialize logging
21341
+ Logger.setEnabled(this.options.enableLogging ?? false);
21191
21342
  this.document = new Document();
21192
21343
  // Apply constructor options to document settings
21193
21344
  this.document.updateSettings({
@@ -21212,7 +21363,8 @@ class PCEditor extends EventEmitter {
21212
21363
  showControlCharacters: options?.showControlCharacters ?? false,
21213
21364
  defaultFont: options?.defaultFont || 'Arial',
21214
21365
  defaultFontSize: options?.defaultFontSize || 12,
21215
- theme: options?.theme || 'light'
21366
+ theme: options?.theme || 'light',
21367
+ enableLogging: options?.enableLogging ?? false
21216
21368
  };
21217
21369
  }
21218
21370
  initialize() {
@@ -21357,6 +21509,10 @@ class PCEditor extends EventEmitter {
21357
21509
  this.canvasManager.on('tablecell-cursor-changed', (data) => {
21358
21510
  this.emit('tablecell-cursor-changed', data);
21359
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
+ });
21360
21516
  this.canvasManager.on('repeating-section-clicked', (data) => {
21361
21517
  // Repeating section clicked - update selection state
21362
21518
  if (data.section && data.section.id) {
@@ -21619,6 +21775,7 @@ class PCEditor extends EventEmitter {
21619
21775
  * This changes which section receives keyboard input and cursor positioning.
21620
21776
  */
21621
21777
  setActiveSection(section) {
21778
+ Logger.log('[pc-editor] setActiveSection', section);
21622
21779
  if (this._activeEditingSection !== section) {
21623
21780
  this._activeEditingSection = section;
21624
21781
  // Delegate to canvas manager which handles the section change and emits events
@@ -21709,6 +21866,7 @@ class PCEditor extends EventEmitter {
21709
21866
  }
21710
21867
  }
21711
21868
  loadDocument(documentData) {
21869
+ Logger.log('[pc-editor] loadDocument');
21712
21870
  if (!this._isReady) {
21713
21871
  throw new Error('Editor is not ready');
21714
21872
  }
@@ -21735,6 +21893,7 @@ class PCEditor extends EventEmitter {
21735
21893
  return this.document.toData();
21736
21894
  }
21737
21895
  bindData(data) {
21896
+ Logger.log('[pc-editor] bindData');
21738
21897
  if (!this._isReady) {
21739
21898
  throw new Error('Editor is not ready');
21740
21899
  }
@@ -21743,6 +21902,7 @@ class PCEditor extends EventEmitter {
21743
21902
  this.emit('data-bound', { data });
21744
21903
  }
21745
21904
  async exportPDF(options) {
21905
+ Logger.log('[pc-editor] exportPDF');
21746
21906
  if (!this._isReady) {
21747
21907
  throw new Error('Editor is not ready');
21748
21908
  }
@@ -21784,6 +21944,7 @@ class PCEditor extends EventEmitter {
21784
21944
  * @returns JSON string representation of the document
21785
21945
  */
21786
21946
  saveDocument() {
21947
+ Logger.log('[pc-editor] saveDocument');
21787
21948
  if (!this._isReady) {
21788
21949
  throw new Error('Editor is not ready');
21789
21950
  }
@@ -21795,6 +21956,7 @@ class PCEditor extends EventEmitter {
21795
21956
  * @param filename Optional filename (defaults to 'document.pceditor.json')
21796
21957
  */
21797
21958
  saveDocumentToFile(filename = 'document.pceditor.json') {
21959
+ Logger.log('[pc-editor] saveDocumentToFile', filename);
21798
21960
  const jsonString = this.saveDocument();
21799
21961
  const blob = new Blob([jsonString], { type: 'application/json' });
21800
21962
  const url = URL.createObjectURL(blob);
@@ -21812,6 +21974,7 @@ class PCEditor extends EventEmitter {
21812
21974
  * @param jsonString JSON string representation of the document
21813
21975
  */
21814
21976
  loadDocumentFromJSON(jsonString) {
21977
+ Logger.log('[pc-editor] loadDocumentFromJSON');
21815
21978
  if (!this._isReady) {
21816
21979
  throw new Error('Editor is not ready');
21817
21980
  }
@@ -21832,6 +21995,7 @@ class PCEditor extends EventEmitter {
21832
21995
  * @returns Promise that resolves when loading is complete
21833
21996
  */
21834
21997
  async loadDocumentFromFile(file) {
21998
+ Logger.log('[pc-editor] loadDocumentFromFile', file.name);
21835
21999
  if (!this._isReady) {
21836
22000
  throw new Error('Editor is not ready');
21837
22001
  }
@@ -21920,22 +22084,25 @@ class PCEditor extends EventEmitter {
21920
22084
  // Version compatibility check
21921
22085
  const [major] = doc.version.split('.').map(Number);
21922
22086
  if (major > 1) {
21923
- console.warn(`Document version ${doc.version} may not be fully compatible with this editor`);
22087
+ Logger.warn(`[pc-editor] Document version ${doc.version} may not be fully compatible with this editor`);
21924
22088
  }
21925
22089
  }
21926
22090
  selectElement(elementId) {
22091
+ Logger.log('[pc-editor] selectElement', elementId);
21927
22092
  if (!this._isReady) {
21928
22093
  throw new Error('Editor is not ready');
21929
22094
  }
21930
22095
  this.canvasManager.selectElement(elementId);
21931
22096
  }
21932
22097
  clearSelection() {
22098
+ Logger.log('[pc-editor] clearSelection');
21933
22099
  if (!this._isReady) {
21934
22100
  throw new Error('Editor is not ready');
21935
22101
  }
21936
22102
  this.canvasManager.clearSelection();
21937
22103
  }
21938
22104
  removeEmbeddedObject(objectId) {
22105
+ Logger.log('[pc-editor] removeEmbeddedObject', objectId);
21939
22106
  if (!this._isReady) {
21940
22107
  throw new Error('Editor is not ready');
21941
22108
  }
@@ -21945,6 +22112,7 @@ class PCEditor extends EventEmitter {
21945
22112
  * Undo the last operation.
21946
22113
  */
21947
22114
  undo() {
22115
+ Logger.log('[pc-editor] undo');
21948
22116
  if (!this._isReady)
21949
22117
  return;
21950
22118
  const success = this.transactionManager.undo();
@@ -21957,6 +22125,7 @@ class PCEditor extends EventEmitter {
21957
22125
  * Redo the last undone operation.
21958
22126
  */
21959
22127
  redo() {
22128
+ Logger.log('[pc-editor] redo');
21960
22129
  if (!this._isReady)
21961
22130
  return;
21962
22131
  const success = this.transactionManager.redo();
@@ -21990,16 +22159,19 @@ class PCEditor extends EventEmitter {
21990
22159
  this.transactionManager.setMaxHistory(count);
21991
22160
  }
21992
22161
  zoomIn() {
22162
+ Logger.log('[pc-editor] zoomIn');
21993
22163
  if (!this._isReady)
21994
22164
  return;
21995
22165
  this.canvasManager.zoomIn();
21996
22166
  }
21997
22167
  zoomOut() {
22168
+ Logger.log('[pc-editor] zoomOut');
21998
22169
  if (!this._isReady)
21999
22170
  return;
22000
22171
  this.canvasManager.zoomOut();
22001
22172
  }
22002
22173
  setZoom(level) {
22174
+ Logger.log('[pc-editor] setZoom', level);
22003
22175
  if (!this._isReady)
22004
22176
  return;
22005
22177
  this.canvasManager.setZoom(level);
@@ -22036,6 +22208,7 @@ class PCEditor extends EventEmitter {
22036
22208
  return this.canvasManager.getContentOffset();
22037
22209
  }
22038
22210
  fitToWidth() {
22211
+ Logger.log('[pc-editor] fitToWidth');
22039
22212
  if (!this._isReady)
22040
22213
  return;
22041
22214
  this.canvasManager.fitToWidth();
@@ -22044,23 +22217,27 @@ class PCEditor extends EventEmitter {
22044
22217
  * Force a re-render of the canvas.
22045
22218
  */
22046
22219
  render() {
22220
+ Logger.log('[pc-editor] render');
22047
22221
  if (!this._isReady)
22048
22222
  return;
22049
22223
  this.canvasManager.render();
22050
22224
  }
22051
22225
  fitToPage() {
22226
+ Logger.log('[pc-editor] fitToPage');
22052
22227
  if (!this._isReady)
22053
22228
  return;
22054
22229
  this.canvasManager.fitToPage();
22055
22230
  }
22056
22231
  // Layout control methods
22057
22232
  setAutoFlow(enabled) {
22233
+ Logger.log('[pc-editor] setAutoFlow', enabled);
22058
22234
  if (!this._isReady) {
22059
22235
  throw new Error('Editor is not ready');
22060
22236
  }
22061
22237
  this.layoutEngine.setAutoFlow(enabled);
22062
22238
  }
22063
22239
  reflowDocument() {
22240
+ Logger.log('[pc-editor] reflowDocument');
22064
22241
  if (!this._isReady) {
22065
22242
  throw new Error('Editor is not ready');
22066
22243
  }
@@ -22102,6 +22279,7 @@ class PCEditor extends EventEmitter {
22102
22279
  };
22103
22280
  }
22104
22281
  addPage() {
22282
+ Logger.log('[pc-editor] addPage');
22105
22283
  if (!this._isReady) {
22106
22284
  throw new Error('Editor is not ready');
22107
22285
  }
@@ -22113,6 +22291,7 @@ class PCEditor extends EventEmitter {
22113
22291
  this.canvasManager.setDocument(this.document);
22114
22292
  }
22115
22293
  removePage(pageId) {
22294
+ Logger.log('[pc-editor] removePage', pageId);
22116
22295
  if (!this._isReady) {
22117
22296
  throw new Error('Editor is not ready');
22118
22297
  }
@@ -22187,7 +22366,7 @@ class PCEditor extends EventEmitter {
22187
22366
  }
22188
22367
  // Use the unified focus system to get the currently focused control
22189
22368
  const focusedControl = this.canvasManager.getFocusedControl();
22190
- console.log('[PCEditor.handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22369
+ Logger.log('[pc-editor:handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22191
22370
  if (!focusedControl)
22192
22371
  return;
22193
22372
  // Vertical navigation needs layout context - handle specially
@@ -22211,9 +22390,9 @@ class PCEditor extends EventEmitter {
22211
22390
  }
22212
22391
  }
22213
22392
  // Delegate to the focused control's handleKeyDown
22214
- console.log('[PCEditor.handleKeyDown] Calling focusedControl.handleKeyDown');
22393
+ Logger.log('[pc-editor:handleKeyDown] Calling focusedControl.handleKeyDown');
22215
22394
  const handled = focusedControl.handleKeyDown(e);
22216
- console.log('[PCEditor.handleKeyDown] handled:', handled);
22395
+ Logger.log('[pc-editor:handleKeyDown] handled:', handled);
22217
22396
  if (handled) {
22218
22397
  this.canvasManager.render();
22219
22398
  // Handle text box-specific post-processing
@@ -22577,6 +22756,7 @@ class PCEditor extends EventEmitter {
22577
22756
  * This is useful when UI controls have stolen focus.
22578
22757
  */
22579
22758
  applyFormattingWithFallback(start, end, formatting) {
22759
+ Logger.log('[pc-editor] applyFormattingWithFallback', start, end, formatting);
22580
22760
  // Try current context first
22581
22761
  let flowingContent = this.getEditingFlowingContent();
22582
22762
  // Fall back to saved context
@@ -22777,6 +22957,7 @@ class PCEditor extends EventEmitter {
22777
22957
  * Works for body text, text boxes, and table cells.
22778
22958
  */
22779
22959
  setUnifiedAlignment(alignment) {
22960
+ Logger.log('[pc-editor] setUnifiedAlignment', alignment);
22780
22961
  const flowingContent = this.getEditingFlowingContent();
22781
22962
  if (!flowingContent) {
22782
22963
  throw new Error('No text is being edited');
@@ -22793,6 +22974,7 @@ class PCEditor extends EventEmitter {
22793
22974
  this.canvasManager.render();
22794
22975
  }
22795
22976
  insertText(text) {
22977
+ Logger.log('[pc-editor] insertText', text);
22796
22978
  if (!this._isReady) {
22797
22979
  throw new Error('Editor is not ready');
22798
22980
  }
@@ -22809,6 +22991,7 @@ class PCEditor extends EventEmitter {
22809
22991
  return flowingContent ? flowingContent.getText() : '';
22810
22992
  }
22811
22993
  setFlowingText(text) {
22994
+ Logger.log('[pc-editor] setFlowingText');
22812
22995
  if (!this._isReady) {
22813
22996
  throw new Error('Editor is not ready');
22814
22997
  }
@@ -22822,6 +23005,7 @@ class PCEditor extends EventEmitter {
22822
23005
  * Works for body, header, footer, text boxes, and table cells.
22823
23006
  */
22824
23007
  setCursorPosition(position) {
23008
+ Logger.log('[pc-editor] setCursorPosition', position);
22825
23009
  if (!this._isReady) {
22826
23010
  throw new Error('Editor is not ready');
22827
23011
  }
@@ -22867,6 +23051,7 @@ class PCEditor extends EventEmitter {
22867
23051
  * Works for body, header, footer, text boxes, and table cells.
22868
23052
  */
22869
23053
  insertEmbeddedObject(object, position = 'inline') {
23054
+ Logger.log('[pc-editor] insertEmbeddedObject', object.id, position);
22870
23055
  if (!this._isReady) {
22871
23056
  throw new Error('Editor is not ready');
22872
23057
  }
@@ -22893,6 +23078,7 @@ class PCEditor extends EventEmitter {
22893
23078
  * Works for body, header, footer, text boxes, and table cells.
22894
23079
  */
22895
23080
  insertSubstitutionField(fieldName, config) {
23081
+ Logger.log('[pc-editor] insertSubstitutionField', fieldName);
22896
23082
  if (!this._isReady) {
22897
23083
  throw new Error('Editor is not ready');
22898
23084
  }
@@ -22918,6 +23104,7 @@ class PCEditor extends EventEmitter {
22918
23104
  * @param displayFormat Optional format string (e.g., "Page %d" where %d is replaced by page number)
22919
23105
  */
22920
23106
  insertPageNumberField(displayFormat) {
23107
+ Logger.log('[pc-editor] insertPageNumberField');
22921
23108
  if (!this._isReady) {
22922
23109
  throw new Error('Editor is not ready');
22923
23110
  }
@@ -22943,6 +23130,7 @@ class PCEditor extends EventEmitter {
22943
23130
  * @param displayFormat Optional format string (e.g., "of %d" where %d is replaced by page count)
22944
23131
  */
22945
23132
  insertPageCountField(displayFormat) {
23133
+ Logger.log('[pc-editor] insertPageCountField');
22946
23134
  if (!this._isReady) {
22947
23135
  throw new Error('Editor is not ready');
22948
23136
  }
@@ -22968,6 +23156,7 @@ class PCEditor extends EventEmitter {
22968
23156
  * or table cells are not recommended as these don't span pages.
22969
23157
  */
22970
23158
  insertPageBreak() {
23159
+ Logger.log('[pc-editor] insertPageBreak');
22971
23160
  if (!this._isReady) {
22972
23161
  throw new Error('Editor is not ready');
22973
23162
  }
@@ -23116,7 +23305,7 @@ class PCEditor extends EventEmitter {
23116
23305
  // Find the text box in all flowing contents
23117
23306
  const textBox = this.findTextBoxById(textBoxId);
23118
23307
  if (!textBox) {
23119
- console.warn(`[PCEditor.updateTextBox] Text box not found: ${textBoxId}`);
23308
+ Logger.warn(`[pc-editor:updateTextBox] Text box not found: ${textBoxId}`);
23120
23309
  return false;
23121
23310
  }
23122
23311
  // Apply updates
@@ -23179,7 +23368,7 @@ class PCEditor extends EventEmitter {
23179
23368
  // Find the image in all flowing contents
23180
23369
  const image = this.findImageById(imageId);
23181
23370
  if (!image) {
23182
- console.warn(`[PCEditor.updateImage] Image not found: ${imageId}`);
23371
+ Logger.warn(`[pc-editor:updateImage] Image not found: ${imageId}`);
23183
23372
  return false;
23184
23373
  }
23185
23374
  // Apply updates
@@ -23213,7 +23402,7 @@ class PCEditor extends EventEmitter {
23213
23402
  return false;
23214
23403
  const image = this.findImageById(imageId);
23215
23404
  if (!image) {
23216
- console.warn(`[PCEditor.setImageSource] Image not found: ${imageId}`);
23405
+ Logger.warn(`[pc-editor:setImageSource] Image not found: ${imageId}`);
23217
23406
  return false;
23218
23407
  }
23219
23408
  image.setSource(dataUrl, options);
@@ -23293,6 +23482,39 @@ class PCEditor extends EventEmitter {
23293
23482
  table.removeColumn(colIndex);
23294
23483
  this.canvasManager.render();
23295
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
+ }
23296
23518
  /**
23297
23519
  * Begin a compound operation. Groups multiple mutations into a single undo entry.
23298
23520
  * Call endCompoundOperation after making changes.
@@ -23340,7 +23562,7 @@ class PCEditor extends EventEmitter {
23340
23562
  * @deprecated Use insertEmbeddedObject instead
23341
23563
  */
23342
23564
  insertInlineElement(_elementData, _position = 'inline') {
23343
- console.warn('insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23565
+ Logger.warn('[pc-editor] insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23344
23566
  }
23345
23567
  /**
23346
23568
  * Apply merge data to substitute all substitution fields with their values.
@@ -24065,18 +24287,37 @@ class PCEditor extends EventEmitter {
24065
24287
  return this.document.bodyFlowingContent.getParagraphBoundaries();
24066
24288
  }
24067
24289
  /**
24068
- * Create a repeating section in the body content.
24069
- * Note: Repeating sections are only supported in the body, not in header/footer.
24070
- * @param startIndex Text index at paragraph start (must be at a paragraph boundary)
24071
- * @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
24290
+ * Create a repeating section.
24291
+ *
24292
+ * If a table is currently being edited (focused), creates a table row loop
24293
+ * based on the focused cell's row. In this case, startIndex and endIndex
24294
+ * are ignored — the focused cell's row determines the loop range.
24295
+ *
24296
+ * Otherwise, creates a body text repeating section at the given paragraph boundaries.
24297
+ *
24298
+ * @param startIndex Text index at paragraph start (ignored for table row loops)
24299
+ * @param endIndex Text index at closing paragraph start (ignored for table row loops)
24072
24300
  * @param fieldPath The field path to the array to loop over (e.g., "items")
24073
- * @returns The created section, or null if boundaries are invalid
24301
+ * @returns The created section/loop, or null if creation failed
24074
24302
  */
24075
24303
  createRepeatingSection(startIndex, endIndex, fieldPath) {
24076
24304
  if (!this._isReady) {
24077
24305
  throw new Error('Editor is not ready');
24078
24306
  }
24307
+ // If a table is focused, create a row loop instead of a text repeating section
24308
+ const focusedTable = this.getFocusedTable();
24309
+ if (focusedTable && focusedTable.focusedCell) {
24310
+ Logger.log('[pc-editor] createRepeatingSection → table row loop', fieldPath);
24311
+ const row = focusedTable.focusedCell.row;
24312
+ const loop = focusedTable.createRowLoop(row, row, fieldPath);
24313
+ if (loop) {
24314
+ this.canvasManager.render();
24315
+ this.emit('table-row-loop-added', { table: focusedTable, loop });
24316
+ }
24317
+ return null; // Row loops are not RepeatingSections, return null
24318
+ }
24079
24319
  // Repeating sections only work in body (document-level)
24320
+ Logger.log('[pc-editor] createRepeatingSection', startIndex, endIndex, fieldPath);
24080
24321
  const section = this.document.bodyFlowingContent.createRepeatingSection(startIndex, endIndex, fieldPath);
24081
24322
  if (section) {
24082
24323
  this.canvasManager.render();
@@ -24454,7 +24695,7 @@ class PCEditor extends EventEmitter {
24454
24695
  createEmbeddedObjectFromData(data) {
24455
24696
  const object = EmbeddedObjectFactory.tryCreate(data);
24456
24697
  if (!object) {
24457
- console.warn('Unknown object type:', data.objectType);
24698
+ Logger.warn('[pc-editor] Unknown object type:', data.objectType);
24458
24699
  }
24459
24700
  return object;
24460
24701
  }
@@ -24483,6 +24724,13 @@ class PCEditor extends EventEmitter {
24483
24724
  return false;
24484
24725
  }
24485
24726
  }
24727
+ /**
24728
+ * Enable or disable verbose logging.
24729
+ * When disabled (default), only errors are logged to the console.
24730
+ */
24731
+ setLogging(enabled) {
24732
+ Logger.setEnabled(enabled);
24733
+ }
24486
24734
  destroy() {
24487
24735
  this.disableTextInput();
24488
24736
  if (this.canvasManager) {
@@ -25625,33 +25873,38 @@ class DocumentInfoPane extends BasePane {
25625
25873
  }
25626
25874
  createContent() {
25627
25875
  const container = document.createElement('div');
25628
- container.className = 'pc-pane-info-list';
25876
+ container.className = 'pc-pane-label-value-grid';
25629
25877
  // Page count
25630
- const countRow = this.createInfoRow('Pages', '0');
25631
- this.pageCountEl = countRow.querySelector('.pc-pane-info-value');
25632
- container.appendChild(countRow);
25878
+ container.appendChild(this.createLabel('Pages:'));
25879
+ this.pageCountEl = this.createValue('0');
25880
+ container.appendChild(this.pageCountEl);
25881
+ container.appendChild(this.createSpacer());
25633
25882
  // Page size
25634
- const sizeRow = this.createInfoRow('Size', '-');
25635
- this.pageSizeEl = sizeRow.querySelector('.pc-pane-info-value');
25636
- container.appendChild(sizeRow);
25883
+ container.appendChild(this.createLabel('Size:'));
25884
+ this.pageSizeEl = this.createValue('-');
25885
+ container.appendChild(this.pageSizeEl);
25886
+ container.appendChild(this.createSpacer());
25637
25887
  // Page orientation
25638
- const orientationRow = this.createInfoRow('Orientation', '-');
25639
- this.pageOrientationEl = orientationRow.querySelector('.pc-pane-info-value');
25640
- container.appendChild(orientationRow);
25888
+ container.appendChild(this.createLabel('Orientation:'));
25889
+ this.pageOrientationEl = this.createValue('-');
25890
+ container.appendChild(this.pageOrientationEl);
25891
+ container.appendChild(this.createSpacer());
25641
25892
  return container;
25642
25893
  }
25643
- createInfoRow(label, value) {
25644
- const row = document.createElement('div');
25645
- row.className = 'pc-pane-info';
25646
- const labelEl = document.createElement('span');
25647
- labelEl.className = 'pc-pane-info-label';
25648
- labelEl.textContent = label;
25649
- const valueEl = document.createElement('span');
25650
- valueEl.className = 'pc-pane-info-value';
25651
- valueEl.textContent = value;
25652
- row.appendChild(labelEl);
25653
- row.appendChild(valueEl);
25654
- return row;
25894
+ createLabel(text) {
25895
+ const label = document.createElement('span');
25896
+ label.className = 'pc-pane-label pc-pane-margin-label';
25897
+ label.textContent = text;
25898
+ return label;
25899
+ }
25900
+ createValue(text) {
25901
+ const value = document.createElement('span');
25902
+ value.className = 'pc-pane-info-value';
25903
+ value.textContent = text;
25904
+ return value;
25905
+ }
25906
+ createSpacer() {
25907
+ return document.createElement('div');
25655
25908
  }
25656
25909
  /**
25657
25910
  * Update the displayed information from the editor.
@@ -25831,24 +26084,48 @@ class DocumentSettingsPane extends BasePane {
25831
26084
  const container = document.createElement('div');
25832
26085
  // Margins section
25833
26086
  const marginsSection = this.createSection('Margins (mm)');
26087
+ // Five-column grid: label, edit, label, edit, stretch
25834
26088
  const marginsGrid = document.createElement('div');
25835
- marginsGrid.className = 'pc-pane-margins-grid';
26089
+ marginsGrid.className = 'pc-pane-margins-grid-5col';
25836
26090
  this.marginTopInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25837
26091
  this.marginRightInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25838
26092
  this.marginBottomInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25839
26093
  this.marginLeftInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25840
- marginsGrid.appendChild(this.createFormGroup('Top', this.marginTopInput, { inline: true }));
25841
- marginsGrid.appendChild(this.createFormGroup('Right', this.marginRightInput, { inline: true }));
25842
- marginsGrid.appendChild(this.createFormGroup('Bottom', this.marginBottomInput, { inline: true }));
25843
- marginsGrid.appendChild(this.createFormGroup('Left', this.marginLeftInput, { inline: true }));
26094
+ // Apply margins on blur
26095
+ const applyMargins = () => this.applyMargins();
26096
+ this.marginTopInput.addEventListener('blur', applyMargins);
26097
+ this.marginRightInput.addEventListener('blur', applyMargins);
26098
+ this.marginBottomInput.addEventListener('blur', applyMargins);
26099
+ this.marginLeftInput.addEventListener('blur', applyMargins);
26100
+ this.eventCleanup.push(() => {
26101
+ this.marginTopInput?.removeEventListener('blur', applyMargins);
26102
+ this.marginRightInput?.removeEventListener('blur', applyMargins);
26103
+ this.marginBottomInput?.removeEventListener('blur', applyMargins);
26104
+ this.marginLeftInput?.removeEventListener('blur', applyMargins);
26105
+ });
26106
+ // Row 1: Top / Right
26107
+ const topLabel = this.createMarginLabel('Top:');
26108
+ const rightLabel = this.createMarginLabel('Right:');
26109
+ marginsGrid.appendChild(topLabel);
26110
+ marginsGrid.appendChild(this.marginTopInput);
26111
+ marginsGrid.appendChild(rightLabel);
26112
+ marginsGrid.appendChild(this.marginRightInput);
26113
+ marginsGrid.appendChild(this.createSpacer());
26114
+ // Row 2: Bottom / Left
26115
+ const bottomLabel = this.createMarginLabel('Bottom:');
26116
+ const leftLabel = this.createMarginLabel('Left:');
26117
+ marginsGrid.appendChild(bottomLabel);
26118
+ marginsGrid.appendChild(this.marginBottomInput);
26119
+ marginsGrid.appendChild(leftLabel);
26120
+ marginsGrid.appendChild(this.marginLeftInput);
26121
+ marginsGrid.appendChild(this.createSpacer());
25844
26122
  marginsSection.appendChild(marginsGrid);
25845
- // Apply margins button
25846
- const applyMarginsBtn = this.createButton('Apply Margins');
25847
- this.addButtonListener(applyMarginsBtn, () => this.applyMargins());
25848
- marginsSection.appendChild(applyMarginsBtn);
25849
26123
  container.appendChild(marginsSection);
25850
- // Page size section
25851
- const pageSizeSection = this.createSection();
26124
+ // Page settings section using label-value grid: label, value, stretch
26125
+ const pageSection = this.createSection();
26126
+ const pageGrid = document.createElement('div');
26127
+ pageGrid.className = 'pc-pane-label-value-grid';
26128
+ // Page Size
25852
26129
  this.pageSizeSelect = this.createSelect([
25853
26130
  { value: 'A4', label: 'A4' },
25854
26131
  { value: 'Letter', label: 'Letter' },
@@ -25856,19 +26133,32 @@ class DocumentSettingsPane extends BasePane {
25856
26133
  { value: 'A3', label: 'A3' }
25857
26134
  ], 'A4');
25858
26135
  this.addImmediateApplyListener(this.pageSizeSelect, () => this.applyPageSettings());
25859
- pageSizeSection.appendChild(this.createFormGroup('Page Size', this.pageSizeSelect));
25860
- container.appendChild(pageSizeSection);
25861
- // Orientation section
25862
- const orientationSection = this.createSection();
26136
+ pageGrid.appendChild(this.createMarginLabel('Page Size:'));
26137
+ pageGrid.appendChild(this.pageSizeSelect);
26138
+ pageGrid.appendChild(this.createSpacer());
26139
+ // Orientation
25863
26140
  this.orientationSelect = this.createSelect([
25864
26141
  { value: 'portrait', label: 'Portrait' },
25865
26142
  { value: 'landscape', label: 'Landscape' }
25866
26143
  ], 'portrait');
25867
26144
  this.addImmediateApplyListener(this.orientationSelect, () => this.applyPageSettings());
25868
- orientationSection.appendChild(this.createFormGroup('Orientation', this.orientationSelect));
25869
- container.appendChild(orientationSection);
26145
+ pageGrid.appendChild(this.createMarginLabel('Orientation:'));
26146
+ pageGrid.appendChild(this.orientationSelect);
26147
+ pageGrid.appendChild(this.createSpacer());
26148
+ pageSection.appendChild(pageGrid);
26149
+ container.appendChild(pageSection);
25870
26150
  return container;
25871
26151
  }
26152
+ createMarginLabel(text) {
26153
+ const label = document.createElement('label');
26154
+ label.className = 'pc-pane-label pc-pane-margin-label';
26155
+ label.textContent = text;
26156
+ return label;
26157
+ }
26158
+ createSpacer() {
26159
+ const spacer = document.createElement('div');
26160
+ return spacer;
26161
+ }
25872
26162
  loadSettings() {
25873
26163
  if (!this.editor)
25874
26164
  return;
@@ -26267,9 +26557,15 @@ class FormattingPane extends BasePane {
26267
26557
  this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26268
26558
  this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26269
26559
  }
26560
+ else {
26561
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
26562
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
26563
+ }
26270
26564
  }
26271
26565
  catch {
26272
26566
  // No text editing active
26567
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
26568
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
26273
26569
  }
26274
26570
  }
26275
26571
  getSelection() {
@@ -26408,6 +26704,7 @@ class HyperlinkPane extends BasePane {
26408
26704
  this.titleInput = null;
26409
26705
  this.rangeHint = null;
26410
26706
  this.currentHyperlink = null;
26707
+ this._isUpdating = false;
26411
26708
  this.onApply = options.onApply;
26412
26709
  this.onRemove = options.onRemove;
26413
26710
  }
@@ -26449,15 +26746,21 @@ class HyperlinkPane extends BasePane {
26449
26746
  return container;
26450
26747
  }
26451
26748
  updateFromCursor() {
26452
- if (!this.editor)
26749
+ if (!this.editor || this._isUpdating)
26453
26750
  return;
26454
- const cursorPos = this.editor.getCursorPosition();
26455
- const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26456
- if (hyperlink) {
26457
- this.showHyperlink(hyperlink);
26751
+ this._isUpdating = true;
26752
+ try {
26753
+ const cursorPos = this.editor.getCursorPosition();
26754
+ const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26755
+ if (hyperlink) {
26756
+ this.showHyperlink(hyperlink);
26757
+ }
26758
+ else {
26759
+ this.hideHyperlink();
26760
+ }
26458
26761
  }
26459
- else {
26460
- this.hideHyperlink();
26762
+ finally {
26763
+ this._isUpdating = false;
26461
26764
  }
26462
26765
  }
26463
26766
  showHyperlink(hyperlink) {
@@ -27076,6 +27379,7 @@ class TextBoxPane extends BasePane {
27076
27379
  this.borderColorInput = null;
27077
27380
  this.borderStyleSelect = null;
27078
27381
  this.paddingInput = null;
27382
+ this._isUpdating = false;
27079
27383
  this.currentTextBox = null;
27080
27384
  this.onApplyCallback = options.onApply;
27081
27385
  }
@@ -27149,14 +27453,20 @@ class TextBoxPane extends BasePane {
27149
27453
  return container;
27150
27454
  }
27151
27455
  updateFromSelection() {
27152
- if (!this.editor)
27456
+ if (!this.editor || this._isUpdating)
27153
27457
  return;
27154
- const textBox = this.editor.getSelectedTextBox?.();
27155
- if (textBox && !textBox.editing) {
27156
- this.showTextBox(textBox);
27458
+ this._isUpdating = true;
27459
+ try {
27460
+ const textBox = this.editor.getSelectedTextBox?.();
27461
+ if (textBox && !textBox.editing) {
27462
+ this.showTextBox(textBox);
27463
+ }
27464
+ else {
27465
+ this.hideTextBox();
27466
+ }
27157
27467
  }
27158
- else {
27159
- this.hideTextBox();
27468
+ finally {
27469
+ this._isUpdating = false;
27160
27470
  }
27161
27471
  }
27162
27472
  /**
@@ -27310,6 +27620,7 @@ class ImagePane extends BasePane {
27310
27620
  this.altTextInput = null;
27311
27621
  this.fileInput = null;
27312
27622
  this.currentImage = null;
27623
+ this._isUpdating = false;
27313
27624
  this.maxImageWidth = options.maxImageWidth ?? 400;
27314
27625
  this.maxImageHeight = options.maxImageHeight ?? 400;
27315
27626
  this.onApplyCallback = options.onApply;
@@ -27391,14 +27702,20 @@ class ImagePane extends BasePane {
27391
27702
  return container;
27392
27703
  }
27393
27704
  updateFromSelection() {
27394
- if (!this.editor)
27705
+ if (!this.editor || this._isUpdating)
27395
27706
  return;
27396
- const image = this.editor.getSelectedImage?.();
27397
- if (image) {
27398
- this.showImage(image);
27707
+ this._isUpdating = true;
27708
+ try {
27709
+ const image = this.editor.getSelectedImage?.();
27710
+ if (image) {
27711
+ this.showImage(image);
27712
+ }
27713
+ else {
27714
+ this.hideImage();
27715
+ }
27399
27716
  }
27400
- else {
27401
- this.hideImage();
27717
+ finally {
27718
+ this._isUpdating = false;
27402
27719
  }
27403
27720
  }
27404
27721
  /**
@@ -27563,6 +27880,9 @@ class TablePane extends BasePane {
27563
27880
  // Default controls
27564
27881
  this.defaultPaddingInput = null;
27565
27882
  this.defaultBorderColorInput = null;
27883
+ // Merge/split buttons
27884
+ this.mergeCellsBtn = null;
27885
+ this.splitCellBtn = null;
27566
27886
  // Cell formatting controls
27567
27887
  this.cellBgColorInput = null;
27568
27888
  this.borderTopCheck = null;
@@ -27573,6 +27893,7 @@ class TablePane extends BasePane {
27573
27893
  this.borderColorInput = null;
27574
27894
  this.borderStyleSelect = null;
27575
27895
  this.currentTable = null;
27896
+ this._isUpdating = false;
27576
27897
  this.onApplyCallback = options.onApply;
27577
27898
  }
27578
27899
  attach(options) {
@@ -27581,12 +27902,12 @@ class TablePane extends BasePane {
27581
27902
  // Listen for selection/focus changes
27582
27903
  const updateHandler = () => this.updateFromFocusedTable();
27583
27904
  this.editor.on('selection-change', updateHandler);
27584
- this.editor.on('table-cell-focus', updateHandler);
27585
- this.editor.on('table-cell-selection', updateHandler);
27905
+ this.editor.on('tablecell-cursor-changed', updateHandler);
27906
+ this.editor.on('table-cell-selection-changed', updateHandler);
27586
27907
  this.eventCleanup.push(() => {
27587
27908
  this.editor?.off('selection-change', updateHandler);
27588
- this.editor?.off('table-cell-focus', updateHandler);
27589
- this.editor?.off('table-cell-selection', updateHandler);
27909
+ this.editor?.off('tablecell-cursor-changed', updateHandler);
27910
+ this.editor?.off('table-cell-selection-changed', updateHandler);
27590
27911
  });
27591
27912
  // Initial update
27592
27913
  this.updateFromFocusedTable();
@@ -27655,6 +27976,17 @@ class TablePane extends BasePane {
27655
27976
  const cellSection = this.createSection('Cell Formatting');
27656
27977
  this.cellSelectionDisplay = this.createHint('No cell selected');
27657
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);
27658
27990
  // Background
27659
27991
  this.cellBgColorInput = this.createColorInput('#ffffff');
27660
27992
  cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
@@ -27711,14 +28043,20 @@ class TablePane extends BasePane {
27711
28043
  return container;
27712
28044
  }
27713
28045
  updateFromFocusedTable() {
27714
- if (!this.editor)
28046
+ if (!this.editor || this._isUpdating)
27715
28047
  return;
27716
- const table = this.editor.getFocusedTable();
27717
- if (table) {
27718
- this.showTable(table);
28048
+ this._isUpdating = true;
28049
+ try {
28050
+ const table = this.editor.getFocusedTable();
28051
+ if (table) {
28052
+ this.showTable(table);
28053
+ }
28054
+ else {
28055
+ this.hideTable();
28056
+ }
27719
28057
  }
27720
- else {
27721
- this.hideTable();
28058
+ finally {
28059
+ this._isUpdating = false;
27722
28060
  }
27723
28061
  }
27724
28062
  /**
@@ -27765,6 +28103,15 @@ class TablePane extends BasePane {
27765
28103
  return;
27766
28104
  const focusedCell = table.focusedCell;
27767
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
+ }
27768
28115
  if (selectedRange) {
27769
28116
  const count = (selectedRange.end.row - selectedRange.start.row + 1) *
27770
28117
  (selectedRange.end.col - selectedRange.start.col + 1);
@@ -27922,6 +28269,21 @@ class TablePane extends BasePane {
27922
28269
  hasTable() {
27923
28270
  return this.currentTable !== null;
27924
28271
  }
28272
+ doMergeCells() {
28273
+ if (!this.editor || !this.currentTable)
28274
+ return;
28275
+ this.editor.tableMergeCells(this.currentTable);
28276
+ this.updateFromFocusedTable();
28277
+ }
28278
+ doSplitCell() {
28279
+ if (!this.editor || !this.currentTable)
28280
+ return;
28281
+ const focused = this.currentTable.focusedCell;
28282
+ if (!focused)
28283
+ return;
28284
+ this.editor.tableSplitCell(this.currentTable, focused.row, focused.col);
28285
+ this.updateFromFocusedTable();
28286
+ }
27925
28287
  /**
27926
28288
  * Update the pane from current editor state.
27927
28289
  */
@@ -27930,5 +28292,5 @@ class TablePane extends BasePane {
27930
28292
  }
27931
28293
  }
27932
28294
 
27933
- export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
28295
+ export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, Logger, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
27934
28296
  //# sourceMappingURL=pc-editor.esm.js.map