@productcloudos/editor 1.0.3 → 1.0.5

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 (64) hide show
  1. package/README.md +1 -0
  2. package/dist/pc-editor.esm.js +3206 -146
  3. package/dist/pc-editor.esm.js.map +1 -1
  4. package/dist/pc-editor.js +3219 -145
  5. package/dist/pc-editor.js.map +1 -1
  6. package/dist/pc-editor.min.js +1 -1
  7. package/dist/pc-editor.min.js.map +1 -1
  8. package/dist/types/lib/core/PCEditor.d.ts +85 -5
  9. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  10. package/dist/types/lib/import/PDFParser.d.ts.map +1 -1
  11. package/dist/types/lib/index.d.ts +3 -0
  12. package/dist/types/lib/index.d.ts.map +1 -1
  13. package/dist/types/lib/objects/EmbeddedObjectFactory.d.ts.map +1 -1
  14. package/dist/types/lib/objects/ImageObject.d.ts +11 -0
  15. package/dist/types/lib/objects/ImageObject.d.ts.map +1 -1
  16. package/dist/types/lib/objects/TextBoxObject.d.ts +5 -2
  17. package/dist/types/lib/objects/TextBoxObject.d.ts.map +1 -1
  18. package/dist/types/lib/objects/table/TableCell.d.ts.map +1 -1
  19. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  20. package/dist/types/lib/objects/table/TableRow.d.ts.map +1 -1
  21. package/dist/types/lib/objects/table/types.d.ts +15 -15
  22. package/dist/types/lib/objects/table/types.d.ts.map +1 -1
  23. package/dist/types/lib/panes/BasePane.d.ts +117 -0
  24. package/dist/types/lib/panes/BasePane.d.ts.map +1 -0
  25. package/dist/types/lib/panes/DocumentInfoPane.d.ts +26 -0
  26. package/dist/types/lib/panes/DocumentInfoPane.d.ts.map +1 -0
  27. package/dist/types/lib/panes/DocumentSettingsPane.d.ts +30 -0
  28. package/dist/types/lib/panes/DocumentSettingsPane.d.ts.map +1 -0
  29. package/dist/types/lib/panes/FormattingPane.d.ts +82 -0
  30. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -0
  31. package/dist/types/lib/panes/HyperlinkPane.d.ts +67 -0
  32. package/dist/types/lib/panes/HyperlinkPane.d.ts.map +1 -0
  33. package/dist/types/lib/panes/ImagePane.d.ts +80 -0
  34. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -0
  35. package/dist/types/lib/panes/MergeDataPane.d.ts +55 -0
  36. package/dist/types/lib/panes/MergeDataPane.d.ts.map +1 -0
  37. package/dist/types/lib/panes/RepeatingSectionPane.d.ts +62 -0
  38. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -0
  39. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts +65 -0
  40. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts.map +1 -0
  41. package/dist/types/lib/panes/TablePane.d.ts +91 -0
  42. package/dist/types/lib/panes/TablePane.d.ts.map +1 -0
  43. package/dist/types/lib/panes/TableRowLoopPane.d.ts +68 -0
  44. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -0
  45. package/dist/types/lib/panes/TextBoxPane.d.ts +68 -0
  46. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -0
  47. package/dist/types/lib/panes/ViewSettingsPane.d.ts +52 -0
  48. package/dist/types/lib/panes/ViewSettingsPane.d.ts.map +1 -0
  49. package/dist/types/lib/panes/index.d.ts +34 -0
  50. package/dist/types/lib/panes/index.d.ts.map +1 -0
  51. package/dist/types/lib/panes/types.d.ts +111 -0
  52. package/dist/types/lib/panes/types.d.ts.map +1 -0
  53. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  54. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  55. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  56. package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
  57. package/dist/types/lib/text/TextFormatting.d.ts +21 -1
  58. package/dist/types/lib/text/TextFormatting.d.ts.map +1 -1
  59. package/dist/types/lib/types/index.d.ts +2 -0
  60. package/dist/types/lib/types/index.d.ts.map +1 -1
  61. package/dist/types/lib/undo/transaction/MutationUndo.d.ts.map +1 -1
  62. package/dist/types/lib/utils/logger.d.ts +20 -0
  63. package/dist/types/lib/utils/logger.d.ts.map +1 -0
  64. 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
  }
@@ -843,6 +844,20 @@ class TextFormattingManager extends EventEmitter {
843
844
  this.emit('formatting-changed', { start, end, formatting });
844
845
  }
845
846
  }
847
+ /**
848
+ * Set formatting at a specific position, replacing all existing formatting.
849
+ * Unlike applyFormatting which merges, this replaces completely.
850
+ * Used by undo operations to restore exact previous state.
851
+ * @param position Character position
852
+ * @param formatting Complete formatting to set
853
+ * @param silent If true, don't emit the formatting-changed event
854
+ */
855
+ setFormattingAt(position, formatting, silent = false) {
856
+ this.formatting.set(position, { ...formatting });
857
+ if (!silent) {
858
+ this.emit('formatting-changed', { start: position, end: position + 1, formatting });
859
+ }
860
+ }
846
861
  /**
847
862
  * Remove formatting from a range, reverting to default.
848
863
  */
@@ -896,6 +911,43 @@ class TextFormattingManager extends EventEmitter {
896
911
  getAllFormatting() {
897
912
  return new Map(this.formatting);
898
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
+ }
899
951
  /**
900
952
  * Restore formatting from a map (for deserialization).
901
953
  */
@@ -3799,6 +3851,35 @@ class ImageObject extends BaseEmbeddedObject {
3799
3851
  get hasError() {
3800
3852
  return this._error;
3801
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
+ }
3802
3883
  loadImage() {
3803
3884
  if (!this._src) {
3804
3885
  this._error = true;
@@ -4694,12 +4775,10 @@ class TextBoxObject extends BaseEmbeddedObject {
4694
4775
  this.editing = false;
4695
4776
  }
4696
4777
  toData() {
4697
- // Serialize formatting map to array of [index, style] pairs
4698
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
4699
- const formattingEntries = [];
4700
- formattingMap.forEach((value, key) => {
4701
- formattingEntries.push([key, { ...value }]);
4702
- });
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 }]);
4703
4782
  // Get substitution fields as array
4704
4783
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
4705
4784
  return {
@@ -4758,12 +4837,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4758
4837
  this._border = { ...boxData.border };
4759
4838
  if (boxData.padding !== undefined)
4760
4839
  this._padding = boxData.padding;
4761
- // Restore formatting runs
4762
- 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) {
4763
4842
  const formattingManager = this._flowingContent.getFormattingManager();
4764
4843
  formattingManager.clear();
4765
- for (const [index, style] of boxData.formattingRuns) {
4766
- 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
+ }
4767
4853
  }
4768
4854
  }
4769
4855
  // Restore substitution fields
@@ -4913,15 +4999,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4913
4999
  }
4914
5000
  /**
4915
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.
4916
5005
  * @param point Point in canvas coordinates
4917
- * @param pageIndex The page index (ignored for text boxes)
5006
+ * @param _pageIndex The page index (ignored for text boxes)
4918
5007
  */
4919
- containsPointInRegion(point, pageIndex) {
4920
- const bounds = this.getRegionBounds(pageIndex);
4921
- if (!bounds)
5008
+ containsPointInRegion(point, _pageIndex) {
5009
+ if (!this._renderedPosition)
4922
5010
  return false;
4923
- return point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
4924
- 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;
4925
5015
  }
4926
5016
  /**
4927
5017
  * Trigger a reflow of text in this text box.
@@ -5006,6 +5096,41 @@ function getVerticalPadding(padding) {
5006
5096
  return padding.top + padding.bottom;
5007
5097
  }
5008
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
+
5009
5134
  /**
5010
5135
  * TableCell - A cell within a table that contains editable text.
5011
5136
  * Implements EditableTextRegion for unified text interaction.
@@ -5056,7 +5181,7 @@ class TableCell extends EventEmitter {
5056
5181
  });
5057
5182
  // Prevent embedded objects in table cells (only substitution fields allowed)
5058
5183
  this._flowingContent.insertEmbeddedObject = () => {
5059
- 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.');
5060
5185
  };
5061
5186
  // Set initial content
5062
5187
  if (config.content) {
@@ -5373,7 +5498,7 @@ class TableCell extends EventEmitter {
5373
5498
  this._reflowDirty = false;
5374
5499
  this._lastReflowWidth = width;
5375
5500
  this._cachedContentHeight = null; // Clear cached height since lines changed
5376
- 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);
5377
5502
  }
5378
5503
  /**
5379
5504
  * Mark this cell as needing reflow.
@@ -5411,7 +5536,7 @@ class TableCell extends EventEmitter {
5411
5536
  return this._editing && this._flowingContent.hasFocus();
5412
5537
  }
5413
5538
  handleKeyDown(e) {
5414
- 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());
5415
5540
  if (!this._editing)
5416
5541
  return false;
5417
5542
  // Let parent table handle Tab navigation
@@ -5419,9 +5544,9 @@ class TableCell extends EventEmitter {
5419
5544
  return false; // Not handled - parent will handle
5420
5545
  }
5421
5546
  // Delegate to FlowingTextContent
5422
- console.log('[TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5547
+ Logger.log('[pc-editor:TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5423
5548
  const handled = this._flowingContent.handleKeyDown(e);
5424
- console.log('[TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5549
+ Logger.log('[pc-editor:TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5425
5550
  return handled;
5426
5551
  }
5427
5552
  onCursorBlink(handler) {
@@ -5493,28 +5618,49 @@ class TableCell extends EventEmitter {
5493
5618
  // Serialization
5494
5619
  // ============================================
5495
5620
  toData() {
5496
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
5497
- const formattingRuns = [];
5498
- formattingMap.forEach((style, index) => {
5499
- formattingRuns.push([index, { ...style }]);
5500
- });
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]);
5501
5624
  // Get substitution fields for serialization
5502
5625
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
5503
- return {
5504
- id: this._id,
5505
- rowSpan: this._rowSpan,
5506
- colSpan: this._colSpan,
5507
- backgroundColor: this._backgroundColor,
5508
- border: this._border,
5509
- padding: this._padding,
5510
- verticalAlign: this._verticalAlign,
5511
- content: this._flowingContent.getText(),
5512
- fontFamily: this._fontFamily,
5513
- fontSize: this._fontSize,
5514
- color: this._color,
5515
- formattingRuns: formattingRuns.length > 0 ? formattingRuns : undefined,
5516
- substitutionFields: fields.length > 0 ? fields : undefined
5517
- };
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;
5518
5664
  }
5519
5665
  static fromData(data) {
5520
5666
  const cell = new TableCell({
@@ -5530,14 +5676,19 @@ class TableCell extends EventEmitter {
5530
5676
  fontSize: data.fontSize,
5531
5677
  color: data.color
5532
5678
  });
5533
- // Restore formatting runs
5534
- 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) {
5535
5681
  const formattingManager = cell._flowingContent.getFormattingManager();
5536
- const formattingMap = new Map();
5537
- for (const [index, style] of data.formattingRuns) {
5538
- 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
+ }
5539
5691
  }
5540
- formattingManager.setAllFormatting(formattingMap);
5541
5692
  }
5542
5693
  // Restore substitution fields
5543
5694
  if (data.substitutionFields && Array.isArray(data.substitutionFields)) {
@@ -5771,13 +5922,17 @@ class TableRow extends EventEmitter {
5771
5922
  // Serialization
5772
5923
  // ============================================
5773
5924
  toData() {
5774
- return {
5775
- id: this._id,
5776
- height: this._height,
5777
- minHeight: this._minHeight,
5778
- isHeader: this._isHeader,
5925
+ const data = {
5779
5926
  cells: this._cells.map(cell => cell.toData())
5780
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;
5781
5936
  }
5782
5937
  static fromData(data) {
5783
5938
  const row = new TableRow({
@@ -6111,7 +6266,7 @@ class TableObject extends BaseEmbeddedObject {
6111
6266
  set position(value) {
6112
6267
  // Tables only support block positioning - ignore any attempt to set other modes
6113
6268
  if (value !== 'block') {
6114
- 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}'.`);
6115
6270
  }
6116
6271
  // Always set to block
6117
6272
  super.position = 'block';
@@ -6650,24 +6805,24 @@ class TableObject extends BaseEmbeddedObject {
6650
6805
  createRowLoop(startRowIndex, endRowIndex, fieldPath) {
6651
6806
  // Validate range
6652
6807
  if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
6653
- console.warn('[TableObject.createRowLoop] Invalid row range');
6808
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Invalid row range');
6654
6809
  return null;
6655
6810
  }
6656
6811
  if (startRowIndex > endRowIndex) {
6657
- console.warn('[TableObject.createRowLoop] Start index must be <= end index');
6812
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Start index must be <= end index');
6658
6813
  return null;
6659
6814
  }
6660
6815
  // Check for overlap with existing loops
6661
6816
  for (const existingLoop of this._rowLoops.values()) {
6662
6817
  if (this.loopRangesOverlap(startRowIndex, endRowIndex, existingLoop.startRowIndex, existingLoop.endRowIndex)) {
6663
- console.warn('[TableObject.createRowLoop] Loop range overlaps with existing loop');
6818
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop range overlaps with existing loop');
6664
6819
  return null;
6665
6820
  }
6666
6821
  }
6667
6822
  // Check that loop rows are not header rows
6668
6823
  for (let i = startRowIndex; i <= endRowIndex; i++) {
6669
6824
  if (this._rows[i]?.isHeader) {
6670
- console.warn('[TableObject.createRowLoop] Loop rows cannot be header rows');
6825
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop rows cannot be header rows');
6671
6826
  return null;
6672
6827
  }
6673
6828
  }
@@ -7473,7 +7628,7 @@ class TableObject extends BaseEmbeddedObject {
7473
7628
  return this._editing;
7474
7629
  }
7475
7630
  handleKeyDown(e) {
7476
- 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);
7477
7632
  if (!this._editing)
7478
7633
  return false;
7479
7634
  // Handle Tab navigation
@@ -8261,14 +8416,20 @@ class EmbeddedObjectFactory {
8261
8416
  border: data.data.border,
8262
8417
  padding: data.data.padding
8263
8418
  });
8264
- // Restore formatting runs if present
8419
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
8265
8420
  if (data.data.formattingRuns && Array.isArray(data.data.formattingRuns)) {
8266
- const formattingManager = textBox.flowingContent.getFormattingManager();
8267
- const formattingMap = new Map();
8268
- for (const [index, style] of data.data.formattingRuns) {
8269
- formattingMap.set(index, style);
8421
+ const runs = data.data.formattingRuns;
8422
+ if (runs.length > 0) {
8423
+ const formattingManager = textBox.flowingContent.getFormattingManager();
8424
+ const textLength = textBox.flowingContent.getText().length;
8425
+ for (let i = 0; i < runs.length; i++) {
8426
+ const [startIndex, style] = runs[i];
8427
+ const nextIndex = i + 1 < runs.length ? runs[i + 1][0] : textLength;
8428
+ if (startIndex < nextIndex) {
8429
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
8430
+ }
8431
+ }
8270
8432
  }
8271
- formattingManager.setAllFormatting(formattingMap);
8272
8433
  }
8273
8434
  // Restore substitution fields if present
8274
8435
  if (data.data.substitutionFields && Array.isArray(data.data.substitutionFields)) {
@@ -9245,9 +9406,9 @@ class FlowingTextContent extends EventEmitter {
9245
9406
  * @returns true if the event was handled, false otherwise
9246
9407
  */
9247
9408
  handleKeyDown(e) {
9248
- console.log('[FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9409
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9249
9410
  if (!this._hasFocus) {
9250
- console.log('[FlowingTextContent.handleKeyDown] No focus, returning false');
9411
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] No focus, returning false');
9251
9412
  return false;
9252
9413
  }
9253
9414
  switch (e.key) {
@@ -9762,46 +9923,19 @@ class FlowingTextContent extends EventEmitter {
9762
9923
  toData() {
9763
9924
  // Serialize text content
9764
9925
  const text = this.textState.getText();
9765
- // Serialize text formatting as runs - only output when format changes
9766
- // This optimizes document size by not storing redundant formatting entries
9767
- const formattingRuns = [];
9768
- const defaultFormat = this.formatting.defaultFormatting;
9769
- let lastFormat = null;
9770
- for (let i = 0; i < text.length; i++) {
9771
- const currentFormat = this.formatting.getFormattingAt(i);
9772
- // Check if formatting changed from previous character
9773
- const formatChanged = lastFormat === null ||
9774
- currentFormat.fontFamily !== lastFormat.fontFamily ||
9775
- currentFormat.fontSize !== lastFormat.fontSize ||
9776
- currentFormat.fontWeight !== lastFormat.fontWeight ||
9777
- currentFormat.fontStyle !== lastFormat.fontStyle ||
9778
- currentFormat.color !== lastFormat.color ||
9779
- currentFormat.backgroundColor !== lastFormat.backgroundColor;
9780
- if (formatChanged) {
9781
- // Only output if different from default (to further reduce size)
9782
- const isDefault = currentFormat.fontFamily === defaultFormat.fontFamily &&
9783
- currentFormat.fontSize === defaultFormat.fontSize &&
9784
- currentFormat.fontWeight === defaultFormat.fontWeight &&
9785
- currentFormat.fontStyle === defaultFormat.fontStyle &&
9786
- currentFormat.color === defaultFormat.color &&
9787
- currentFormat.backgroundColor === defaultFormat.backgroundColor;
9788
- // Always output first run if it's not default, or output when format changes
9789
- if (!isDefault || formattingRuns.length > 0) {
9790
- formattingRuns.push({
9791
- index: i,
9792
- formatting: {
9793
- fontFamily: currentFormat.fontFamily,
9794
- fontSize: currentFormat.fontSize,
9795
- fontWeight: currentFormat.fontWeight,
9796
- fontStyle: currentFormat.fontStyle,
9797
- color: currentFormat.color,
9798
- backgroundColor: currentFormat.backgroundColor
9799
- }
9800
- });
9801
- }
9802
- lastFormat = currentFormat;
9926
+ // Serialize text formatting as compressed runs (only at change boundaries)
9927
+ const compressedRuns = this.formatting.getCompressedRuns(text.length);
9928
+ const formattingRuns = compressedRuns.map(run => ({
9929
+ index: run.index,
9930
+ formatting: {
9931
+ fontFamily: run.formatting.fontFamily,
9932
+ fontSize: run.formatting.fontSize,
9933
+ fontWeight: run.formatting.fontWeight,
9934
+ fontStyle: run.formatting.fontStyle,
9935
+ color: run.formatting.color,
9936
+ backgroundColor: run.formatting.backgroundColor
9803
9937
  }
9804
- }
9938
+ }));
9805
9939
  // Serialize paragraph formatting
9806
9940
  const paragraphFormatting = this.paragraphFormatting.toJSON();
9807
9941
  // Serialize substitution fields
@@ -9897,7 +10031,7 @@ class FlowingTextContent extends EventEmitter {
9897
10031
  content.getEmbeddedObjectManager().insert(object, ref.textIndex);
9898
10032
  }
9899
10033
  else {
9900
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10034
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9901
10035
  }
9902
10036
  }
9903
10037
  }
@@ -9951,7 +10085,7 @@ class FlowingTextContent extends EventEmitter {
9951
10085
  this.embeddedObjects.insert(object, ref.textIndex);
9952
10086
  }
9953
10087
  else {
9954
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10088
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9955
10089
  }
9956
10090
  }
9957
10091
  }
@@ -12490,7 +12624,7 @@ class FlowingTextRenderer extends EventEmitter {
12490
12624
  updateResizeHandleTargets(selectedObjects) {
12491
12625
  // Clear existing resize handle targets
12492
12626
  this._hitTestManager.clearCategory('resize-handles');
12493
- console.log('[updateResizeHandleTargets] selectedObjects:', selectedObjects.length);
12627
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets selectedObjects:', selectedObjects.length);
12494
12628
  // Register resize handles for each selected object
12495
12629
  for (const object of selectedObjects) {
12496
12630
  if (!object.resizable)
@@ -12503,7 +12637,7 @@ class FlowingTextRenderer extends EventEmitter {
12503
12637
  // For regular objects, use renderedPosition
12504
12638
  const pos = object.renderedPosition;
12505
12639
  const pageIndex = object.renderedPageIndex;
12506
- console.log('[updateResizeHandleTargets] object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12640
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12507
12641
  if (pos && pageIndex >= 0) {
12508
12642
  this.registerObjectResizeHandles(object, pageIndex, pos);
12509
12643
  }
@@ -15149,7 +15283,7 @@ class CanvasManager extends EventEmitter {
15149
15283
  this.emit('element-removed', { elementId: objectId });
15150
15284
  }
15151
15285
  selectElement(elementId) {
15152
- console.log('Selecting element:', elementId);
15286
+ Logger.log('[pc-editor:CanvasManager] Selecting element:', elementId);
15153
15287
  this.selectedElements.add(elementId);
15154
15288
  // Update embedded object's selected state
15155
15289
  const flowingContents = [
@@ -15161,12 +15295,12 @@ class CanvasManager extends EventEmitter {
15161
15295
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15162
15296
  for (const [, obj] of embeddedObjects.entries()) {
15163
15297
  if (obj.id === elementId) {
15164
- console.log('Found embedded object to select:', obj.id);
15298
+ Logger.log('[pc-editor:CanvasManager] Found embedded object to select:', obj.id);
15165
15299
  obj.selected = true;
15166
15300
  }
15167
15301
  }
15168
15302
  }
15169
- console.log('Selected elements after selection:', Array.from(this.selectedElements));
15303
+ Logger.log('[pc-editor:CanvasManager] Selected elements after selection:', Array.from(this.selectedElements));
15170
15304
  this.render();
15171
15305
  this.updateResizeHandleHitTargets();
15172
15306
  this.emit('selection-change', { selectedElements: Array.from(this.selectedElements) });
@@ -15216,10 +15350,10 @@ class CanvasManager extends EventEmitter {
15216
15350
  this.flowingTextRenderer.updateResizeHandleTargets(selectedObjects);
15217
15351
  }
15218
15352
  clearSelection() {
15219
- console.log('clearSelection called, current selected elements:', Array.from(this.selectedElements));
15353
+ Logger.log('[pc-editor:CanvasManager] clearSelection called, current selected elements:', Array.from(this.selectedElements));
15220
15354
  // Clear selected state on all embedded objects
15221
15355
  this.selectedElements.forEach(elementId => {
15222
- console.log('Clearing selection for element:', elementId);
15356
+ Logger.log('[pc-editor:CanvasManager] Clearing selection for element:', elementId);
15223
15357
  // Check embedded objects in all flowing content sources (body, header, footer)
15224
15358
  const flowingContents = [
15225
15359
  this.document.bodyFlowingContent,
@@ -15230,7 +15364,7 @@ class CanvasManager extends EventEmitter {
15230
15364
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15231
15365
  for (const [, embeddedObj] of embeddedObjects.entries()) {
15232
15366
  if (embeddedObj.id === elementId) {
15233
- console.log('Clearing selection on embedded object:', elementId);
15367
+ Logger.log('[pc-editor:CanvasManager] Clearing selection on embedded object:', elementId);
15234
15368
  embeddedObj.selected = false;
15235
15369
  }
15236
15370
  }
@@ -15238,7 +15372,7 @@ class CanvasManager extends EventEmitter {
15238
15372
  });
15239
15373
  this.selectedElements.clear();
15240
15374
  this.selectedSectionId = null;
15241
- console.log('About to render after clearing selection...');
15375
+ Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
15242
15376
  this.render();
15243
15377
  this.updateResizeHandleHitTargets();
15244
15378
  this.emit('selection-change', { selectedElements: [] });
@@ -15487,7 +15621,7 @@ class CanvasManager extends EventEmitter {
15487
15621
  });
15488
15622
  // Handle substitution field clicks
15489
15623
  this.flowingTextRenderer.on('substitution-field-clicked', (data) => {
15490
- console.log('[substitution-field-clicked] Field:', data.field?.fieldName, 'Section:', data.section);
15624
+ Logger.log('[pc-editor:CanvasManager] substitution-field-clicked Field:', data.field?.fieldName, 'Section:', data.section);
15491
15625
  // Emit event for external handling (e.g., showing field properties panel)
15492
15626
  this.emit('substitution-field-clicked', data);
15493
15627
  });
@@ -15962,14 +16096,15 @@ class CanvasManager extends EventEmitter {
15962
16096
  this.editingTextBox = textBox;
15963
16097
  this._editingTextBoxPageId = pageId || null;
15964
16098
  if (textBox) {
15965
- // Use the unified focus system to handle focus/blur and cursor blink
15966
- // This blurs the previous control, hiding its cursor
15967
- this.setFocus(textBox);
15968
16099
  // Clear selection in main flowing content
15969
16100
  this.document.bodyFlowingContent.clearSelection();
15970
- // Select the text box
16101
+ // Select the text box visually (this calls setFocus(null) internally,
16102
+ // so we must set focus to the text box AFTER this call)
15971
16103
  this.clearSelection();
15972
16104
  this.selectInlineElement({ type: 'embedded-object', object: textBox, textIndex: textBox.textIndex });
16105
+ // Now set focus to the text box for editing — must be AFTER selectInlineElement
16106
+ // because selectBaseEmbeddedObject calls setFocus(null) which would undo it
16107
+ this.setFocus(textBox);
15973
16108
  this.emit('textbox-editing-started', { textBox });
15974
16109
  }
15975
16110
  else {
@@ -16708,7 +16843,15 @@ class PDFGenerator {
16708
16843
  // Check if it's a data URL we can embed
16709
16844
  if (src.startsWith('data:')) {
16710
16845
  try {
16711
- const embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16846
+ let embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16847
+ // If the format isn't directly supported (e.g., SVG, WebP, GIF),
16848
+ // convert to PNG via canvas and try again
16849
+ if (!embeddedImage) {
16850
+ const pngDataUrl = image.toPngDataUrl();
16851
+ if (pngDataUrl) {
16852
+ embeddedImage = await this.embedImageFromDataUrl(pdfDoc, pngDataUrl);
16853
+ }
16854
+ }
16712
16855
  if (embeddedImage) {
16713
16856
  // Calculate draw position/size based on fit mode
16714
16857
  const drawParams = this.calculateImageDrawParams(embeddedImage.width, embeddedImage.height, image.width, image.height, image.fit);
@@ -16731,7 +16874,7 @@ class PDFGenerator {
16731
16874
  }
16732
16875
  }
16733
16876
  catch (e) {
16734
- console.warn('Failed to embed image:', e);
16877
+ Logger.warn('[pc-editor:PDFGenerator] Failed to embed image:', e);
16735
16878
  }
16736
16879
  }
16737
16880
  // Fallback: draw placeholder rectangle for images we can't embed
@@ -18759,7 +18902,7 @@ class MutationUndo {
18759
18902
  this.undoTableStructure(mutation);
18760
18903
  break;
18761
18904
  default:
18762
- console.warn('Unknown mutation type for undo:', mutation.type);
18905
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for undo:', mutation.type);
18763
18906
  }
18764
18907
  }
18765
18908
  /**
@@ -18809,7 +18952,7 @@ class MutationUndo {
18809
18952
  this.redoTableStructure(mutation);
18810
18953
  break;
18811
18954
  default:
18812
- console.warn('Unknown mutation type for redo:', mutation.type);
18955
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for redo:', mutation.type);
18813
18956
  }
18814
18957
  }
18815
18958
  // --- Text Mutations ---
@@ -18839,11 +18982,11 @@ class MutationUndo {
18839
18982
  const data = mutation.data;
18840
18983
  // Restore deleted text
18841
18984
  content.insertTextAt(data.position, data.deletedText);
18842
- // Restore formatting
18985
+ // Restore formatting using setFormattingAt to replace completely
18843
18986
  if (data.deletedFormatting) {
18844
18987
  const fm = content.getFormattingManager();
18845
18988
  data.deletedFormatting.forEach((style, offset) => {
18846
- fm.applyFormatting(data.position + offset, data.position + offset + 1, style);
18989
+ fm.setFormattingAt(data.position + offset, style, true);
18847
18990
  });
18848
18991
  }
18849
18992
  // Restore substitution fields
@@ -18869,9 +19012,10 @@ class MutationUndo {
18869
19012
  return;
18870
19013
  const data = mutation.data;
18871
19014
  const fm = content.getFormattingManager();
18872
- // Restore previous formatting
19015
+ // Restore previous formatting using setFormattingAt to replace completely
19016
+ // (not merge, which would leave properties like backgroundColor intact)
18873
19017
  data.previousFormatting.forEach((style, offset) => {
18874
- fm.applyFormatting(data.start + offset, data.start + offset + 1, style);
19018
+ fm.setFormattingAt(data.start + offset, style, true);
18875
19019
  });
18876
19020
  }
18877
19021
  redoFormat(mutation) {
@@ -20140,13 +20284,13 @@ class PDFParser {
20140
20284
  }
20141
20285
  catch {
20142
20286
  // Skip images that fail to extract
20143
- console.warn(`Failed to extract image: ${imageName}`);
20287
+ Logger.warn(`[pc-editor:PDFParser] Failed to extract image: ${imageName}`);
20144
20288
  }
20145
20289
  }
20146
20290
  }
20147
20291
  }
20148
20292
  catch (error) {
20149
- console.warn('Image extraction failed:', error);
20293
+ Logger.warn('[pc-editor:PDFParser] Image extraction failed:', error);
20150
20294
  }
20151
20295
  return images;
20152
20296
  }
@@ -21173,6 +21317,8 @@ class PCEditor extends EventEmitter {
21173
21317
  }
21174
21318
  this.container = container;
21175
21319
  this.options = this.mergeOptions(options);
21320
+ // Initialize logging
21321
+ Logger.setEnabled(this.options.enableLogging ?? false);
21176
21322
  this.document = new Document();
21177
21323
  // Apply constructor options to document settings
21178
21324
  this.document.updateSettings({
@@ -21197,7 +21343,8 @@ class PCEditor extends EventEmitter {
21197
21343
  showControlCharacters: options?.showControlCharacters ?? false,
21198
21344
  defaultFont: options?.defaultFont || 'Arial',
21199
21345
  defaultFontSize: options?.defaultFontSize || 12,
21200
- theme: options?.theme || 'light'
21346
+ theme: options?.theme || 'light',
21347
+ enableLogging: options?.enableLogging ?? false
21201
21348
  };
21202
21349
  }
21203
21350
  initialize() {
@@ -21604,6 +21751,7 @@ class PCEditor extends EventEmitter {
21604
21751
  * This changes which section receives keyboard input and cursor positioning.
21605
21752
  */
21606
21753
  setActiveSection(section) {
21754
+ Logger.log('[pc-editor] setActiveSection', section);
21607
21755
  if (this._activeEditingSection !== section) {
21608
21756
  this._activeEditingSection = section;
21609
21757
  // Delegate to canvas manager which handles the section change and emits events
@@ -21694,6 +21842,7 @@ class PCEditor extends EventEmitter {
21694
21842
  }
21695
21843
  }
21696
21844
  loadDocument(documentData) {
21845
+ Logger.log('[pc-editor] loadDocument');
21697
21846
  if (!this._isReady) {
21698
21847
  throw new Error('Editor is not ready');
21699
21848
  }
@@ -21720,6 +21869,7 @@ class PCEditor extends EventEmitter {
21720
21869
  return this.document.toData();
21721
21870
  }
21722
21871
  bindData(data) {
21872
+ Logger.log('[pc-editor] bindData');
21723
21873
  if (!this._isReady) {
21724
21874
  throw new Error('Editor is not ready');
21725
21875
  }
@@ -21728,6 +21878,7 @@ class PCEditor extends EventEmitter {
21728
21878
  this.emit('data-bound', { data });
21729
21879
  }
21730
21880
  async exportPDF(options) {
21881
+ Logger.log('[pc-editor] exportPDF');
21731
21882
  if (!this._isReady) {
21732
21883
  throw new Error('Editor is not ready');
21733
21884
  }
@@ -21769,6 +21920,7 @@ class PCEditor extends EventEmitter {
21769
21920
  * @returns JSON string representation of the document
21770
21921
  */
21771
21922
  saveDocument() {
21923
+ Logger.log('[pc-editor] saveDocument');
21772
21924
  if (!this._isReady) {
21773
21925
  throw new Error('Editor is not ready');
21774
21926
  }
@@ -21780,6 +21932,7 @@ class PCEditor extends EventEmitter {
21780
21932
  * @param filename Optional filename (defaults to 'document.pceditor.json')
21781
21933
  */
21782
21934
  saveDocumentToFile(filename = 'document.pceditor.json') {
21935
+ Logger.log('[pc-editor] saveDocumentToFile', filename);
21783
21936
  const jsonString = this.saveDocument();
21784
21937
  const blob = new Blob([jsonString], { type: 'application/json' });
21785
21938
  const url = URL.createObjectURL(blob);
@@ -21797,6 +21950,7 @@ class PCEditor extends EventEmitter {
21797
21950
  * @param jsonString JSON string representation of the document
21798
21951
  */
21799
21952
  loadDocumentFromJSON(jsonString) {
21953
+ Logger.log('[pc-editor] loadDocumentFromJSON');
21800
21954
  if (!this._isReady) {
21801
21955
  throw new Error('Editor is not ready');
21802
21956
  }
@@ -21817,6 +21971,7 @@ class PCEditor extends EventEmitter {
21817
21971
  * @returns Promise that resolves when loading is complete
21818
21972
  */
21819
21973
  async loadDocumentFromFile(file) {
21974
+ Logger.log('[pc-editor] loadDocumentFromFile', file.name);
21820
21975
  if (!this._isReady) {
21821
21976
  throw new Error('Editor is not ready');
21822
21977
  }
@@ -21905,22 +22060,25 @@ class PCEditor extends EventEmitter {
21905
22060
  // Version compatibility check
21906
22061
  const [major] = doc.version.split('.').map(Number);
21907
22062
  if (major > 1) {
21908
- console.warn(`Document version ${doc.version} may not be fully compatible with this editor`);
22063
+ Logger.warn(`[pc-editor] Document version ${doc.version} may not be fully compatible with this editor`);
21909
22064
  }
21910
22065
  }
21911
22066
  selectElement(elementId) {
22067
+ Logger.log('[pc-editor] selectElement', elementId);
21912
22068
  if (!this._isReady) {
21913
22069
  throw new Error('Editor is not ready');
21914
22070
  }
21915
22071
  this.canvasManager.selectElement(elementId);
21916
22072
  }
21917
22073
  clearSelection() {
22074
+ Logger.log('[pc-editor] clearSelection');
21918
22075
  if (!this._isReady) {
21919
22076
  throw new Error('Editor is not ready');
21920
22077
  }
21921
22078
  this.canvasManager.clearSelection();
21922
22079
  }
21923
22080
  removeEmbeddedObject(objectId) {
22081
+ Logger.log('[pc-editor] removeEmbeddedObject', objectId);
21924
22082
  if (!this._isReady) {
21925
22083
  throw new Error('Editor is not ready');
21926
22084
  }
@@ -21930,6 +22088,7 @@ class PCEditor extends EventEmitter {
21930
22088
  * Undo the last operation.
21931
22089
  */
21932
22090
  undo() {
22091
+ Logger.log('[pc-editor] undo');
21933
22092
  if (!this._isReady)
21934
22093
  return;
21935
22094
  const success = this.transactionManager.undo();
@@ -21942,6 +22101,7 @@ class PCEditor extends EventEmitter {
21942
22101
  * Redo the last undone operation.
21943
22102
  */
21944
22103
  redo() {
22104
+ Logger.log('[pc-editor] redo');
21945
22105
  if (!this._isReady)
21946
22106
  return;
21947
22107
  const success = this.transactionManager.redo();
@@ -21975,16 +22135,19 @@ class PCEditor extends EventEmitter {
21975
22135
  this.transactionManager.setMaxHistory(count);
21976
22136
  }
21977
22137
  zoomIn() {
22138
+ Logger.log('[pc-editor] zoomIn');
21978
22139
  if (!this._isReady)
21979
22140
  return;
21980
22141
  this.canvasManager.zoomIn();
21981
22142
  }
21982
22143
  zoomOut() {
22144
+ Logger.log('[pc-editor] zoomOut');
21983
22145
  if (!this._isReady)
21984
22146
  return;
21985
22147
  this.canvasManager.zoomOut();
21986
22148
  }
21987
22149
  setZoom(level) {
22150
+ Logger.log('[pc-editor] setZoom', level);
21988
22151
  if (!this._isReady)
21989
22152
  return;
21990
22153
  this.canvasManager.setZoom(level);
@@ -22021,6 +22184,7 @@ class PCEditor extends EventEmitter {
22021
22184
  return this.canvasManager.getContentOffset();
22022
22185
  }
22023
22186
  fitToWidth() {
22187
+ Logger.log('[pc-editor] fitToWidth');
22024
22188
  if (!this._isReady)
22025
22189
  return;
22026
22190
  this.canvasManager.fitToWidth();
@@ -22029,23 +22193,27 @@ class PCEditor extends EventEmitter {
22029
22193
  * Force a re-render of the canvas.
22030
22194
  */
22031
22195
  render() {
22196
+ Logger.log('[pc-editor] render');
22032
22197
  if (!this._isReady)
22033
22198
  return;
22034
22199
  this.canvasManager.render();
22035
22200
  }
22036
22201
  fitToPage() {
22202
+ Logger.log('[pc-editor] fitToPage');
22037
22203
  if (!this._isReady)
22038
22204
  return;
22039
22205
  this.canvasManager.fitToPage();
22040
22206
  }
22041
22207
  // Layout control methods
22042
22208
  setAutoFlow(enabled) {
22209
+ Logger.log('[pc-editor] setAutoFlow', enabled);
22043
22210
  if (!this._isReady) {
22044
22211
  throw new Error('Editor is not ready');
22045
22212
  }
22046
22213
  this.layoutEngine.setAutoFlow(enabled);
22047
22214
  }
22048
22215
  reflowDocument() {
22216
+ Logger.log('[pc-editor] reflowDocument');
22049
22217
  if (!this._isReady) {
22050
22218
  throw new Error('Editor is not ready');
22051
22219
  }
@@ -22087,6 +22255,7 @@ class PCEditor extends EventEmitter {
22087
22255
  };
22088
22256
  }
22089
22257
  addPage() {
22258
+ Logger.log('[pc-editor] addPage');
22090
22259
  if (!this._isReady) {
22091
22260
  throw new Error('Editor is not ready');
22092
22261
  }
@@ -22098,6 +22267,7 @@ class PCEditor extends EventEmitter {
22098
22267
  this.canvasManager.setDocument(this.document);
22099
22268
  }
22100
22269
  removePage(pageId) {
22270
+ Logger.log('[pc-editor] removePage', pageId);
22101
22271
  if (!this._isReady) {
22102
22272
  throw new Error('Editor is not ready');
22103
22273
  }
@@ -22172,7 +22342,7 @@ class PCEditor extends EventEmitter {
22172
22342
  }
22173
22343
  // Use the unified focus system to get the currently focused control
22174
22344
  const focusedControl = this.canvasManager.getFocusedControl();
22175
- console.log('[PCEditor.handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22345
+ Logger.log('[pc-editor:handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22176
22346
  if (!focusedControl)
22177
22347
  return;
22178
22348
  // Vertical navigation needs layout context - handle specially
@@ -22196,9 +22366,9 @@ class PCEditor extends EventEmitter {
22196
22366
  }
22197
22367
  }
22198
22368
  // Delegate to the focused control's handleKeyDown
22199
- console.log('[PCEditor.handleKeyDown] Calling focusedControl.handleKeyDown');
22369
+ Logger.log('[pc-editor:handleKeyDown] Calling focusedControl.handleKeyDown');
22200
22370
  const handled = focusedControl.handleKeyDown(e);
22201
- console.log('[PCEditor.handleKeyDown] handled:', handled);
22371
+ Logger.log('[pc-editor:handleKeyDown] handled:', handled);
22202
22372
  if (handled) {
22203
22373
  this.canvasManager.render();
22204
22374
  // Handle text box-specific post-processing
@@ -22562,6 +22732,7 @@ class PCEditor extends EventEmitter {
22562
22732
  * This is useful when UI controls have stolen focus.
22563
22733
  */
22564
22734
  applyFormattingWithFallback(start, end, formatting) {
22735
+ Logger.log('[pc-editor] applyFormattingWithFallback', start, end, formatting);
22565
22736
  // Try current context first
22566
22737
  let flowingContent = this.getEditingFlowingContent();
22567
22738
  // Fall back to saved context
@@ -22762,6 +22933,7 @@ class PCEditor extends EventEmitter {
22762
22933
  * Works for body text, text boxes, and table cells.
22763
22934
  */
22764
22935
  setUnifiedAlignment(alignment) {
22936
+ Logger.log('[pc-editor] setUnifiedAlignment', alignment);
22765
22937
  const flowingContent = this.getEditingFlowingContent();
22766
22938
  if (!flowingContent) {
22767
22939
  throw new Error('No text is being edited');
@@ -22778,6 +22950,7 @@ class PCEditor extends EventEmitter {
22778
22950
  this.canvasManager.render();
22779
22951
  }
22780
22952
  insertText(text) {
22953
+ Logger.log('[pc-editor] insertText', text);
22781
22954
  if (!this._isReady) {
22782
22955
  throw new Error('Editor is not ready');
22783
22956
  }
@@ -22794,6 +22967,7 @@ class PCEditor extends EventEmitter {
22794
22967
  return flowingContent ? flowingContent.getText() : '';
22795
22968
  }
22796
22969
  setFlowingText(text) {
22970
+ Logger.log('[pc-editor] setFlowingText');
22797
22971
  if (!this._isReady) {
22798
22972
  throw new Error('Editor is not ready');
22799
22973
  }
@@ -22807,6 +22981,7 @@ class PCEditor extends EventEmitter {
22807
22981
  * Works for body, header, footer, text boxes, and table cells.
22808
22982
  */
22809
22983
  setCursorPosition(position) {
22984
+ Logger.log('[pc-editor] setCursorPosition', position);
22810
22985
  if (!this._isReady) {
22811
22986
  throw new Error('Editor is not ready');
22812
22987
  }
@@ -22852,6 +23027,7 @@ class PCEditor extends EventEmitter {
22852
23027
  * Works for body, header, footer, text boxes, and table cells.
22853
23028
  */
22854
23029
  insertEmbeddedObject(object, position = 'inline') {
23030
+ Logger.log('[pc-editor] insertEmbeddedObject', object.id, position);
22855
23031
  if (!this._isReady) {
22856
23032
  throw new Error('Editor is not ready');
22857
23033
  }
@@ -22878,6 +23054,7 @@ class PCEditor extends EventEmitter {
22878
23054
  * Works for body, header, footer, text boxes, and table cells.
22879
23055
  */
22880
23056
  insertSubstitutionField(fieldName, config) {
23057
+ Logger.log('[pc-editor] insertSubstitutionField', fieldName);
22881
23058
  if (!this._isReady) {
22882
23059
  throw new Error('Editor is not ready');
22883
23060
  }
@@ -22903,6 +23080,7 @@ class PCEditor extends EventEmitter {
22903
23080
  * @param displayFormat Optional format string (e.g., "Page %d" where %d is replaced by page number)
22904
23081
  */
22905
23082
  insertPageNumberField(displayFormat) {
23083
+ Logger.log('[pc-editor] insertPageNumberField');
22906
23084
  if (!this._isReady) {
22907
23085
  throw new Error('Editor is not ready');
22908
23086
  }
@@ -22928,6 +23106,7 @@ class PCEditor extends EventEmitter {
22928
23106
  * @param displayFormat Optional format string (e.g., "of %d" where %d is replaced by page count)
22929
23107
  */
22930
23108
  insertPageCountField(displayFormat) {
23109
+ Logger.log('[pc-editor] insertPageCountField');
22931
23110
  if (!this._isReady) {
22932
23111
  throw new Error('Editor is not ready');
22933
23112
  }
@@ -22953,6 +23132,7 @@ class PCEditor extends EventEmitter {
22953
23132
  * or table cells are not recommended as these don't span pages.
22954
23133
  */
22955
23134
  insertPageBreak() {
23135
+ Logger.log('[pc-editor] insertPageBreak');
22956
23136
  if (!this._isReady) {
22957
23137
  throw new Error('Editor is not ready');
22958
23138
  }
@@ -23088,6 +23268,144 @@ class PCEditor extends EventEmitter {
23088
23268
  return null;
23089
23269
  }
23090
23270
  // ============================================
23271
+ // Text Box Update Operations
23272
+ // ============================================
23273
+ /**
23274
+ * Update properties of a text box.
23275
+ * @param textBoxId The ID of the text box to update
23276
+ * @param updates The properties to update
23277
+ */
23278
+ updateTextBox(textBoxId, updates) {
23279
+ if (!this._isReady)
23280
+ return false;
23281
+ // Find the text box in all flowing contents
23282
+ const textBox = this.findTextBoxById(textBoxId);
23283
+ if (!textBox) {
23284
+ Logger.warn(`[pc-editor:updateTextBox] Text box not found: ${textBoxId}`);
23285
+ return false;
23286
+ }
23287
+ // Apply updates
23288
+ if (updates.position !== undefined) {
23289
+ textBox.position = updates.position;
23290
+ }
23291
+ if (updates.relativeOffset !== undefined) {
23292
+ textBox.relativeOffset = updates.relativeOffset;
23293
+ }
23294
+ if (updates.backgroundColor !== undefined) {
23295
+ textBox.backgroundColor = updates.backgroundColor;
23296
+ }
23297
+ if (updates.border !== undefined) {
23298
+ // Merge with existing border
23299
+ const existingBorder = textBox.border;
23300
+ textBox.border = {
23301
+ top: updates.border.top || existingBorder.top,
23302
+ right: updates.border.right || existingBorder.right,
23303
+ bottom: updates.border.bottom || existingBorder.bottom,
23304
+ left: updates.border.left || existingBorder.left
23305
+ };
23306
+ }
23307
+ if (updates.padding !== undefined) {
23308
+ textBox.padding = updates.padding;
23309
+ }
23310
+ this.render();
23311
+ this.emit('textbox-updated', { textBoxId, updates });
23312
+ return true;
23313
+ }
23314
+ /**
23315
+ * Find a text box by ID across all flowing contents.
23316
+ */
23317
+ findTextBoxById(textBoxId) {
23318
+ const flowingContents = [
23319
+ this.document.bodyFlowingContent,
23320
+ this.document.headerFlowingContent,
23321
+ this.document.footerFlowingContent
23322
+ ].filter(Boolean);
23323
+ for (const flowingContent of flowingContents) {
23324
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23325
+ for (const [, obj] of embeddedObjects.entries()) {
23326
+ if (obj.id === textBoxId && obj instanceof TextBoxObject) {
23327
+ return obj;
23328
+ }
23329
+ }
23330
+ }
23331
+ return null;
23332
+ }
23333
+ // ============================================
23334
+ // Image Update Operations
23335
+ // ============================================
23336
+ /**
23337
+ * Update properties of an image.
23338
+ * @param imageId The ID of the image to update
23339
+ * @param updates The properties to update
23340
+ */
23341
+ updateImage(imageId, updates) {
23342
+ if (!this._isReady)
23343
+ return false;
23344
+ // Find the image in all flowing contents
23345
+ const image = this.findImageById(imageId);
23346
+ if (!image) {
23347
+ Logger.warn(`[pc-editor:updateImage] Image not found: ${imageId}`);
23348
+ return false;
23349
+ }
23350
+ // Apply updates
23351
+ if (updates.position !== undefined) {
23352
+ image.position = updates.position;
23353
+ }
23354
+ if (updates.relativeOffset !== undefined) {
23355
+ image.relativeOffset = updates.relativeOffset;
23356
+ }
23357
+ if (updates.fit !== undefined) {
23358
+ image.fit = updates.fit;
23359
+ }
23360
+ if (updates.resizeMode !== undefined) {
23361
+ image.resizeMode = updates.resizeMode;
23362
+ }
23363
+ if (updates.alt !== undefined) {
23364
+ image.alt = updates.alt;
23365
+ }
23366
+ this.render();
23367
+ this.emit('image-updated', { imageId, updates });
23368
+ return true;
23369
+ }
23370
+ /**
23371
+ * Set the source of an image.
23372
+ * @param imageId The ID of the image
23373
+ * @param dataUrl The data URL of the new image source
23374
+ * @param options Optional sizing options
23375
+ */
23376
+ setImageSource(imageId, dataUrl, options) {
23377
+ if (!this._isReady)
23378
+ return false;
23379
+ const image = this.findImageById(imageId);
23380
+ if (!image) {
23381
+ Logger.warn(`[pc-editor:setImageSource] Image not found: ${imageId}`);
23382
+ return false;
23383
+ }
23384
+ image.setSource(dataUrl, options);
23385
+ this.render();
23386
+ this.emit('image-source-changed', { imageId });
23387
+ return true;
23388
+ }
23389
+ /**
23390
+ * Find an image by ID across all flowing contents.
23391
+ */
23392
+ findImageById(imageId) {
23393
+ const flowingContents = [
23394
+ this.document.bodyFlowingContent,
23395
+ this.document.headerFlowingContent,
23396
+ this.document.footerFlowingContent
23397
+ ].filter(Boolean);
23398
+ for (const flowingContent of flowingContents) {
23399
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23400
+ for (const [, obj] of embeddedObjects.entries()) {
23401
+ if (obj.id === imageId && obj instanceof ImageObject) {
23402
+ return obj;
23403
+ }
23404
+ }
23405
+ }
23406
+ return null;
23407
+ }
23408
+ // ============================================
23091
23409
  // Table Structure Operations (with undo support)
23092
23410
  // ============================================
23093
23411
  /**
@@ -23187,7 +23505,7 @@ class PCEditor extends EventEmitter {
23187
23505
  * @deprecated Use insertEmbeddedObject instead
23188
23506
  */
23189
23507
  insertInlineElement(_elementData, _position = 'inline') {
23190
- console.warn('insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23508
+ Logger.warn('[pc-editor] insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23191
23509
  }
23192
23510
  /**
23193
23511
  * Apply merge data to substitute all substitution fields with their values.
@@ -23912,18 +24230,37 @@ class PCEditor extends EventEmitter {
23912
24230
  return this.document.bodyFlowingContent.getParagraphBoundaries();
23913
24231
  }
23914
24232
  /**
23915
- * Create a repeating section in the body content.
23916
- * Note: Repeating sections are only supported in the body, not in header/footer.
23917
- * @param startIndex Text index at paragraph start (must be at a paragraph boundary)
23918
- * @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
24233
+ * Create a repeating section.
24234
+ *
24235
+ * If a table is currently being edited (focused), creates a table row loop
24236
+ * based on the focused cell's row. In this case, startIndex and endIndex
24237
+ * are ignored — the focused cell's row determines the loop range.
24238
+ *
24239
+ * Otherwise, creates a body text repeating section at the given paragraph boundaries.
24240
+ *
24241
+ * @param startIndex Text index at paragraph start (ignored for table row loops)
24242
+ * @param endIndex Text index at closing paragraph start (ignored for table row loops)
23919
24243
  * @param fieldPath The field path to the array to loop over (e.g., "items")
23920
- * @returns The created section, or null if boundaries are invalid
24244
+ * @returns The created section/loop, or null if creation failed
23921
24245
  */
23922
24246
  createRepeatingSection(startIndex, endIndex, fieldPath) {
23923
24247
  if (!this._isReady) {
23924
24248
  throw new Error('Editor is not ready');
23925
24249
  }
24250
+ // If a table is focused, create a row loop instead of a text repeating section
24251
+ const focusedTable = this.getFocusedTable();
24252
+ if (focusedTable && focusedTable.focusedCell) {
24253
+ Logger.log('[pc-editor] createRepeatingSection → table row loop', fieldPath);
24254
+ const row = focusedTable.focusedCell.row;
24255
+ const loop = focusedTable.createRowLoop(row, row, fieldPath);
24256
+ if (loop) {
24257
+ this.canvasManager.render();
24258
+ this.emit('table-row-loop-added', { table: focusedTable, loop });
24259
+ }
24260
+ return null; // Row loops are not RepeatingSections, return null
24261
+ }
23926
24262
  // Repeating sections only work in body (document-level)
24263
+ Logger.log('[pc-editor] createRepeatingSection', startIndex, endIndex, fieldPath);
23927
24264
  const section = this.document.bodyFlowingContent.createRepeatingSection(startIndex, endIndex, fieldPath);
23928
24265
  if (section) {
23929
24266
  this.canvasManager.render();
@@ -24301,7 +24638,7 @@ class PCEditor extends EventEmitter {
24301
24638
  createEmbeddedObjectFromData(data) {
24302
24639
  const object = EmbeddedObjectFactory.tryCreate(data);
24303
24640
  if (!object) {
24304
- console.warn('Unknown object type:', data.objectType);
24641
+ Logger.warn('[pc-editor] Unknown object type:', data.objectType);
24305
24642
  }
24306
24643
  return object;
24307
24644
  }
@@ -24330,6 +24667,13 @@ class PCEditor extends EventEmitter {
24330
24667
  return false;
24331
24668
  }
24332
24669
  }
24670
+ /**
24671
+ * Enable or disable verbose logging.
24672
+ * When disabled (default), only errors are logged to the console.
24673
+ */
24674
+ setLogging(enabled) {
24675
+ Logger.setEnabled(enabled);
24676
+ }
24333
24677
  destroy() {
24334
24678
  this.disableTextInput();
24335
24679
  if (this.canvasManager) {
@@ -25158,5 +25502,2721 @@ class VerticalRuler extends RulerControl {
25158
25502
  }
25159
25503
  }
25160
25504
 
25161
- export { BaseControl, BaseEmbeddedObject, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, HeaderTextRegion, HorizontalRuler, HtmlConverter, ImageObject, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RulerControl, SubstitutionFieldManager, TableCell, TableObject, TableRow, TextBoxObject, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler };
25505
+ /**
25506
+ * BasePane - Abstract base class for editor property panes.
25507
+ *
25508
+ * Panes are property editors that work with PCEditor via the public API only.
25509
+ * They are content-only (no title bar) for flexible layout by consumers.
25510
+ */
25511
+ /**
25512
+ * Abstract base class for editor panes.
25513
+ */
25514
+ class BasePane extends BaseControl {
25515
+ constructor(id, options = {}) {
25516
+ super(id, options);
25517
+ this.sectionElement = null;
25518
+ this.className = options.className || '';
25519
+ }
25520
+ /**
25521
+ * Attach the pane to an editor.
25522
+ */
25523
+ attach(options) {
25524
+ // Store the section element if provided
25525
+ this.sectionElement = options.sectionElement || null;
25526
+ super.attach(options);
25527
+ }
25528
+ /**
25529
+ * Show the pane (and section element if provided).
25530
+ */
25531
+ show() {
25532
+ this._isVisible = true;
25533
+ if (this.sectionElement) {
25534
+ this.sectionElement.style.display = '';
25535
+ }
25536
+ if (this.element) {
25537
+ this.element.style.display = '';
25538
+ this.update();
25539
+ }
25540
+ this.emit('visibility-changed', { visible: true });
25541
+ }
25542
+ /**
25543
+ * Hide the pane (and section element if provided).
25544
+ */
25545
+ hide() {
25546
+ this._isVisible = false;
25547
+ if (this.sectionElement) {
25548
+ this.sectionElement.style.display = 'none';
25549
+ }
25550
+ if (this.element) {
25551
+ this.element.style.display = 'none';
25552
+ }
25553
+ this.emit('visibility-changed', { visible: false });
25554
+ }
25555
+ /**
25556
+ * Create a form group element with label.
25557
+ */
25558
+ createFormGroup(label, inputElement, options) {
25559
+ const group = document.createElement('div');
25560
+ group.className = 'pc-pane-form-group';
25561
+ if (options?.inline) {
25562
+ group.classList.add('pc-pane-form-group--inline');
25563
+ }
25564
+ const labelEl = document.createElement('label');
25565
+ labelEl.className = 'pc-pane-label';
25566
+ labelEl.textContent = label;
25567
+ group.appendChild(labelEl);
25568
+ group.appendChild(inputElement);
25569
+ if (options?.hint) {
25570
+ const hintEl = document.createElement('span');
25571
+ hintEl.className = 'pc-pane-hint';
25572
+ hintEl.textContent = options.hint;
25573
+ group.appendChild(hintEl);
25574
+ }
25575
+ return group;
25576
+ }
25577
+ /**
25578
+ * Create a text input element.
25579
+ */
25580
+ createTextInput(options) {
25581
+ const input = document.createElement('input');
25582
+ input.type = options?.type || 'text';
25583
+ input.className = 'pc-pane-input';
25584
+ if (options?.placeholder) {
25585
+ input.placeholder = options.placeholder;
25586
+ }
25587
+ if (options?.value !== undefined) {
25588
+ input.value = options.value;
25589
+ }
25590
+ return input;
25591
+ }
25592
+ /**
25593
+ * Create a number input element.
25594
+ */
25595
+ createNumberInput(options) {
25596
+ const input = document.createElement('input');
25597
+ input.type = 'number';
25598
+ input.className = 'pc-pane-input pc-pane-input--number';
25599
+ if (options?.min !== undefined)
25600
+ input.min = String(options.min);
25601
+ if (options?.max !== undefined)
25602
+ input.max = String(options.max);
25603
+ if (options?.step !== undefined)
25604
+ input.step = String(options.step);
25605
+ if (options?.value !== undefined)
25606
+ input.value = String(options.value);
25607
+ return input;
25608
+ }
25609
+ /**
25610
+ * Create a select element with options.
25611
+ */
25612
+ createSelect(optionsList, selectedValue) {
25613
+ const select = document.createElement('select');
25614
+ select.className = 'pc-pane-select';
25615
+ for (const opt of optionsList) {
25616
+ const option = document.createElement('option');
25617
+ option.value = opt.value;
25618
+ option.textContent = opt.label;
25619
+ if (opt.value === selectedValue) {
25620
+ option.selected = true;
25621
+ }
25622
+ select.appendChild(option);
25623
+ }
25624
+ return select;
25625
+ }
25626
+ /**
25627
+ * Create a color input element.
25628
+ */
25629
+ createColorInput(value) {
25630
+ const input = document.createElement('input');
25631
+ input.type = 'color';
25632
+ input.className = 'pc-pane-color';
25633
+ if (value) {
25634
+ input.value = value;
25635
+ }
25636
+ return input;
25637
+ }
25638
+ /**
25639
+ * Create a checkbox element.
25640
+ */
25641
+ createCheckbox(label, checked) {
25642
+ const wrapper = document.createElement('label');
25643
+ wrapper.className = 'pc-pane-checkbox';
25644
+ const input = document.createElement('input');
25645
+ input.type = 'checkbox';
25646
+ if (checked) {
25647
+ input.checked = true;
25648
+ }
25649
+ const span = document.createElement('span');
25650
+ span.textContent = label;
25651
+ wrapper.appendChild(input);
25652
+ wrapper.appendChild(span);
25653
+ return wrapper;
25654
+ }
25655
+ /**
25656
+ * Create a button element.
25657
+ */
25658
+ createButton(label, options) {
25659
+ const button = document.createElement('button');
25660
+ button.type = 'button';
25661
+ button.className = 'pc-pane-button';
25662
+ if (options?.variant) {
25663
+ button.classList.add(`pc-pane-button--${options.variant}`);
25664
+ }
25665
+ button.textContent = label;
25666
+ return button;
25667
+ }
25668
+ /**
25669
+ * Create a button group container.
25670
+ */
25671
+ createButtonGroup() {
25672
+ const group = document.createElement('div');
25673
+ group.className = 'pc-pane-button-group';
25674
+ return group;
25675
+ }
25676
+ /**
25677
+ * Create a section divider with optional label.
25678
+ */
25679
+ createSection(label) {
25680
+ const section = document.createElement('div');
25681
+ section.className = 'pc-pane-section';
25682
+ if (label) {
25683
+ const labelEl = document.createElement('div');
25684
+ labelEl.className = 'pc-pane-section-label';
25685
+ labelEl.textContent = label;
25686
+ section.appendChild(labelEl);
25687
+ }
25688
+ return section;
25689
+ }
25690
+ /**
25691
+ * Create a row container for inline elements.
25692
+ */
25693
+ createRow() {
25694
+ const row = document.createElement('div');
25695
+ row.className = 'pc-pane-row';
25696
+ return row;
25697
+ }
25698
+ /**
25699
+ * Create a hint/info text element.
25700
+ */
25701
+ createHint(text) {
25702
+ const hint = document.createElement('div');
25703
+ hint.className = 'pc-pane-hint';
25704
+ hint.textContent = text;
25705
+ return hint;
25706
+ }
25707
+ /**
25708
+ * Add immediate apply listener for text inputs (blur + Enter).
25709
+ */
25710
+ addImmediateApplyListener(element, handler) {
25711
+ const apply = () => {
25712
+ handler(element.value);
25713
+ };
25714
+ // Selects and color inputs: apply on change
25715
+ if (element instanceof HTMLSelectElement ||
25716
+ (element instanceof HTMLInputElement && element.type === 'color')) {
25717
+ element.addEventListener('change', apply);
25718
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25719
+ }
25720
+ else {
25721
+ // Text/number inputs: apply on blur or Enter
25722
+ element.addEventListener('blur', apply);
25723
+ const keyHandler = (e) => {
25724
+ if (e.key === 'Enter') {
25725
+ e.preventDefault();
25726
+ apply();
25727
+ }
25728
+ };
25729
+ element.addEventListener('keydown', keyHandler);
25730
+ this.eventCleanup.push(() => {
25731
+ element.removeEventListener('blur', apply);
25732
+ element.removeEventListener('keydown', keyHandler);
25733
+ });
25734
+ }
25735
+ }
25736
+ /**
25737
+ * Add immediate apply listener for checkbox inputs.
25738
+ */
25739
+ addCheckboxListener(element, handler) {
25740
+ const apply = () => handler(element.checked);
25741
+ element.addEventListener('change', apply);
25742
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25743
+ }
25744
+ /**
25745
+ * Add button click handler with focus steal prevention.
25746
+ */
25747
+ addButtonListener(button, handler) {
25748
+ // Prevent focus steal on mousedown
25749
+ const preventFocus = (e) => {
25750
+ e.preventDefault();
25751
+ this.saveEditorContext();
25752
+ };
25753
+ button.addEventListener('mousedown', preventFocus);
25754
+ button.addEventListener('click', handler);
25755
+ this.eventCleanup.push(() => {
25756
+ button.removeEventListener('mousedown', preventFocus);
25757
+ button.removeEventListener('click', handler);
25758
+ });
25759
+ }
25760
+ /**
25761
+ * Save editor context before UI elements steal focus.
25762
+ */
25763
+ saveEditorContext() {
25764
+ if (this.editor) {
25765
+ this.editor.saveEditingContext();
25766
+ }
25767
+ }
25768
+ /**
25769
+ * Final createElement that wraps content in pane structure.
25770
+ * Content-only, no title bar.
25771
+ */
25772
+ createElement() {
25773
+ const wrapper = document.createElement('div');
25774
+ wrapper.className = 'pc-pane';
25775
+ if (this.className) {
25776
+ wrapper.classList.add(this.className);
25777
+ }
25778
+ wrapper.setAttribute('data-pane-id', this.id);
25779
+ const content = this.createContent();
25780
+ wrapper.appendChild(content);
25781
+ return wrapper;
25782
+ }
25783
+ }
25784
+
25785
+ /**
25786
+ * DocumentInfoPane - Read-only document information display.
25787
+ *
25788
+ * Shows:
25789
+ * - Page count
25790
+ * - Page size
25791
+ * - Page orientation
25792
+ */
25793
+ class DocumentInfoPane extends BasePane {
25794
+ constructor(id = 'document-info') {
25795
+ super(id, { className: 'pc-pane-document-info' });
25796
+ this.pageCountEl = null;
25797
+ this.pageSizeEl = null;
25798
+ this.pageOrientationEl = null;
25799
+ }
25800
+ attach(options) {
25801
+ super.attach(options);
25802
+ // Subscribe to document changes
25803
+ if (this.editor) {
25804
+ const updateHandler = () => this.update();
25805
+ this.editor.on('document-changed', updateHandler);
25806
+ this.editor.on('page-added', updateHandler);
25807
+ this.editor.on('page-removed', updateHandler);
25808
+ this.eventCleanup.push(() => {
25809
+ this.editor?.off('document-changed', updateHandler);
25810
+ this.editor?.off('page-added', updateHandler);
25811
+ this.editor?.off('page-removed', updateHandler);
25812
+ });
25813
+ // Initial update
25814
+ this.update();
25815
+ }
25816
+ }
25817
+ createContent() {
25818
+ const container = document.createElement('div');
25819
+ container.className = 'pc-pane-label-value-grid';
25820
+ // Page count
25821
+ container.appendChild(this.createLabel('Pages:'));
25822
+ this.pageCountEl = this.createValue('0');
25823
+ container.appendChild(this.pageCountEl);
25824
+ container.appendChild(this.createSpacer());
25825
+ // Page size
25826
+ container.appendChild(this.createLabel('Size:'));
25827
+ this.pageSizeEl = this.createValue('-');
25828
+ container.appendChild(this.pageSizeEl);
25829
+ container.appendChild(this.createSpacer());
25830
+ // Page orientation
25831
+ container.appendChild(this.createLabel('Orientation:'));
25832
+ this.pageOrientationEl = this.createValue('-');
25833
+ container.appendChild(this.pageOrientationEl);
25834
+ container.appendChild(this.createSpacer());
25835
+ return container;
25836
+ }
25837
+ createLabel(text) {
25838
+ const label = document.createElement('span');
25839
+ label.className = 'pc-pane-label pc-pane-margin-label';
25840
+ label.textContent = text;
25841
+ return label;
25842
+ }
25843
+ createValue(text) {
25844
+ const value = document.createElement('span');
25845
+ value.className = 'pc-pane-info-value';
25846
+ value.textContent = text;
25847
+ return value;
25848
+ }
25849
+ createSpacer() {
25850
+ return document.createElement('div');
25851
+ }
25852
+ /**
25853
+ * Update the displayed information from the editor.
25854
+ */
25855
+ update() {
25856
+ if (!this.editor)
25857
+ return;
25858
+ const doc = this.editor.getDocument();
25859
+ if (this.pageCountEl) {
25860
+ this.pageCountEl.textContent = doc.pages.length.toString();
25861
+ }
25862
+ if (this.pageSizeEl && doc.settings) {
25863
+ this.pageSizeEl.textContent = doc.settings.pageSize;
25864
+ }
25865
+ if (this.pageOrientationEl && doc.settings) {
25866
+ const orientation = doc.settings.pageOrientation;
25867
+ this.pageOrientationEl.textContent =
25868
+ orientation.charAt(0).toUpperCase() + orientation.slice(1);
25869
+ }
25870
+ }
25871
+ }
25872
+
25873
+ /**
25874
+ * ViewSettingsPane - Toggle buttons for view options.
25875
+ *
25876
+ * Toggles:
25877
+ * - Rulers (requires external callback since rulers are optional controls)
25878
+ * - Control characters
25879
+ * - Margin lines
25880
+ * - Grid
25881
+ */
25882
+ class ViewSettingsPane extends BasePane {
25883
+ constructor(id = 'view-settings', options = {}) {
25884
+ super(id, { className: 'pc-pane-view-settings', ...options });
25885
+ this.rulersBtn = null;
25886
+ this.controlCharsBtn = null;
25887
+ this.marginLinesBtn = null;
25888
+ this.gridBtn = null;
25889
+ this.onToggleRulers = options.onToggleRulers;
25890
+ this.rulersVisible = options.rulersVisible ?? true;
25891
+ }
25892
+ attach(options) {
25893
+ super.attach(options);
25894
+ // Subscribe to editor events
25895
+ if (this.editor) {
25896
+ const updateHandler = () => this.updateButtonStates();
25897
+ this.editor.on('grid-changed', updateHandler);
25898
+ this.editor.on('margin-lines-changed', updateHandler);
25899
+ this.editor.on('control-characters-changed', updateHandler);
25900
+ this.eventCleanup.push(() => {
25901
+ this.editor?.off('grid-changed', updateHandler);
25902
+ this.editor?.off('margin-lines-changed', updateHandler);
25903
+ this.editor?.off('control-characters-changed', updateHandler);
25904
+ });
25905
+ // Initial state
25906
+ this.updateButtonStates();
25907
+ }
25908
+ }
25909
+ createContent() {
25910
+ const container = document.createElement('div');
25911
+ container.className = 'pc-pane-button-group pc-pane-view-toggles';
25912
+ // Rulers toggle (only if callback provided)
25913
+ if (this.onToggleRulers) {
25914
+ this.rulersBtn = this.createToggleButton('Rulers', this.rulersVisible);
25915
+ this.addButtonListener(this.rulersBtn, () => this.toggleRulers());
25916
+ container.appendChild(this.rulersBtn);
25917
+ }
25918
+ // Control characters toggle
25919
+ this.controlCharsBtn = this.createToggleButton('Control Chars', false);
25920
+ this.addButtonListener(this.controlCharsBtn, () => this.toggleControlChars());
25921
+ container.appendChild(this.controlCharsBtn);
25922
+ // Margin lines toggle
25923
+ this.marginLinesBtn = this.createToggleButton('Margin Lines', true);
25924
+ this.addButtonListener(this.marginLinesBtn, () => this.toggleMarginLines());
25925
+ container.appendChild(this.marginLinesBtn);
25926
+ // Grid toggle
25927
+ this.gridBtn = this.createToggleButton('Grid', true);
25928
+ this.addButtonListener(this.gridBtn, () => this.toggleGrid());
25929
+ container.appendChild(this.gridBtn);
25930
+ return container;
25931
+ }
25932
+ createToggleButton(label, active) {
25933
+ const button = document.createElement('button');
25934
+ button.type = 'button';
25935
+ button.className = 'pc-pane-toggle';
25936
+ if (active) {
25937
+ button.classList.add('pc-pane-toggle--active');
25938
+ }
25939
+ button.textContent = label;
25940
+ button.title = `Toggle ${label}`;
25941
+ return button;
25942
+ }
25943
+ toggleRulers() {
25944
+ if (this.onToggleRulers) {
25945
+ this.onToggleRulers();
25946
+ this.rulersVisible = !this.rulersVisible;
25947
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', this.rulersVisible);
25948
+ }
25949
+ }
25950
+ toggleControlChars() {
25951
+ if (!this.editor)
25952
+ return;
25953
+ const current = this.editor.getShowControlCharacters();
25954
+ this.editor.setShowControlCharacters(!current);
25955
+ }
25956
+ toggleMarginLines() {
25957
+ if (!this.editor)
25958
+ return;
25959
+ const current = this.editor.getShowMarginLines();
25960
+ this.editor.setShowMarginLines(!current);
25961
+ }
25962
+ toggleGrid() {
25963
+ if (!this.editor)
25964
+ return;
25965
+ const current = this.editor.getShowGrid();
25966
+ this.editor.setShowGrid(!current);
25967
+ }
25968
+ updateButtonStates() {
25969
+ if (!this.editor)
25970
+ return;
25971
+ if (this.controlCharsBtn) {
25972
+ this.controlCharsBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowControlCharacters());
25973
+ }
25974
+ if (this.marginLinesBtn) {
25975
+ this.marginLinesBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowMarginLines());
25976
+ }
25977
+ if (this.gridBtn) {
25978
+ this.gridBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowGrid());
25979
+ }
25980
+ }
25981
+ /**
25982
+ * Update ruler button state externally (since rulers are external controls).
25983
+ */
25984
+ setRulersVisible(visible) {
25985
+ this.rulersVisible = visible;
25986
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', visible);
25987
+ }
25988
+ /**
25989
+ * Update the pane from current editor state.
25990
+ */
25991
+ update() {
25992
+ this.updateButtonStates();
25993
+ }
25994
+ }
25995
+
25996
+ /**
25997
+ * DocumentSettingsPane - Edit margins, page size, and orientation.
25998
+ *
25999
+ * Uses the PCEditor public API:
26000
+ * - editor.getDocumentSettings()
26001
+ * - editor.updateDocumentSettings()
26002
+ */
26003
+ class DocumentSettingsPane extends BasePane {
26004
+ constructor(id = 'document-settings') {
26005
+ super(id, { className: 'pc-pane-document-settings' });
26006
+ this.marginTopInput = null;
26007
+ this.marginRightInput = null;
26008
+ this.marginBottomInput = null;
26009
+ this.marginLeftInput = null;
26010
+ this.pageSizeSelect = null;
26011
+ this.orientationSelect = null;
26012
+ }
26013
+ attach(options) {
26014
+ super.attach(options);
26015
+ // Load current settings
26016
+ if (this.editor) {
26017
+ this.loadSettings();
26018
+ // Subscribe to document changes
26019
+ const updateHandler = () => this.loadSettings();
26020
+ this.editor.on('document-changed', updateHandler);
26021
+ this.eventCleanup.push(() => {
26022
+ this.editor?.off('document-changed', updateHandler);
26023
+ });
26024
+ }
26025
+ }
26026
+ createContent() {
26027
+ const container = document.createElement('div');
26028
+ // Margins section
26029
+ const marginsSection = this.createSection('Margins (mm)');
26030
+ // Five-column grid: label, edit, label, edit, stretch
26031
+ const marginsGrid = document.createElement('div');
26032
+ marginsGrid.className = 'pc-pane-margins-grid-5col';
26033
+ this.marginTopInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26034
+ this.marginRightInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26035
+ this.marginBottomInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26036
+ this.marginLeftInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26037
+ // Apply margins on blur
26038
+ const applyMargins = () => this.applyMargins();
26039
+ this.marginTopInput.addEventListener('blur', applyMargins);
26040
+ this.marginRightInput.addEventListener('blur', applyMargins);
26041
+ this.marginBottomInput.addEventListener('blur', applyMargins);
26042
+ this.marginLeftInput.addEventListener('blur', applyMargins);
26043
+ this.eventCleanup.push(() => {
26044
+ this.marginTopInput?.removeEventListener('blur', applyMargins);
26045
+ this.marginRightInput?.removeEventListener('blur', applyMargins);
26046
+ this.marginBottomInput?.removeEventListener('blur', applyMargins);
26047
+ this.marginLeftInput?.removeEventListener('blur', applyMargins);
26048
+ });
26049
+ // Row 1: Top / Right
26050
+ const topLabel = this.createMarginLabel('Top:');
26051
+ const rightLabel = this.createMarginLabel('Right:');
26052
+ marginsGrid.appendChild(topLabel);
26053
+ marginsGrid.appendChild(this.marginTopInput);
26054
+ marginsGrid.appendChild(rightLabel);
26055
+ marginsGrid.appendChild(this.marginRightInput);
26056
+ marginsGrid.appendChild(this.createSpacer());
26057
+ // Row 2: Bottom / Left
26058
+ const bottomLabel = this.createMarginLabel('Bottom:');
26059
+ const leftLabel = this.createMarginLabel('Left:');
26060
+ marginsGrid.appendChild(bottomLabel);
26061
+ marginsGrid.appendChild(this.marginBottomInput);
26062
+ marginsGrid.appendChild(leftLabel);
26063
+ marginsGrid.appendChild(this.marginLeftInput);
26064
+ marginsGrid.appendChild(this.createSpacer());
26065
+ marginsSection.appendChild(marginsGrid);
26066
+ container.appendChild(marginsSection);
26067
+ // Page settings section using label-value grid: label, value, stretch
26068
+ const pageSection = this.createSection();
26069
+ const pageGrid = document.createElement('div');
26070
+ pageGrid.className = 'pc-pane-label-value-grid';
26071
+ // Page Size
26072
+ this.pageSizeSelect = this.createSelect([
26073
+ { value: 'A4', label: 'A4' },
26074
+ { value: 'Letter', label: 'Letter' },
26075
+ { value: 'Legal', label: 'Legal' },
26076
+ { value: 'A3', label: 'A3' }
26077
+ ], 'A4');
26078
+ this.addImmediateApplyListener(this.pageSizeSelect, () => this.applyPageSettings());
26079
+ pageGrid.appendChild(this.createMarginLabel('Page Size:'));
26080
+ pageGrid.appendChild(this.pageSizeSelect);
26081
+ pageGrid.appendChild(this.createSpacer());
26082
+ // Orientation
26083
+ this.orientationSelect = this.createSelect([
26084
+ { value: 'portrait', label: 'Portrait' },
26085
+ { value: 'landscape', label: 'Landscape' }
26086
+ ], 'portrait');
26087
+ this.addImmediateApplyListener(this.orientationSelect, () => this.applyPageSettings());
26088
+ pageGrid.appendChild(this.createMarginLabel('Orientation:'));
26089
+ pageGrid.appendChild(this.orientationSelect);
26090
+ pageGrid.appendChild(this.createSpacer());
26091
+ pageSection.appendChild(pageGrid);
26092
+ container.appendChild(pageSection);
26093
+ return container;
26094
+ }
26095
+ createMarginLabel(text) {
26096
+ const label = document.createElement('label');
26097
+ label.className = 'pc-pane-label pc-pane-margin-label';
26098
+ label.textContent = text;
26099
+ return label;
26100
+ }
26101
+ createSpacer() {
26102
+ const spacer = document.createElement('div');
26103
+ return spacer;
26104
+ }
26105
+ loadSettings() {
26106
+ if (!this.editor)
26107
+ return;
26108
+ try {
26109
+ const settings = this.editor.getDocumentSettings();
26110
+ if (this.marginTopInput) {
26111
+ this.marginTopInput.value = settings.margins.top.toString();
26112
+ }
26113
+ if (this.marginRightInput) {
26114
+ this.marginRightInput.value = settings.margins.right.toString();
26115
+ }
26116
+ if (this.marginBottomInput) {
26117
+ this.marginBottomInput.value = settings.margins.bottom.toString();
26118
+ }
26119
+ if (this.marginLeftInput) {
26120
+ this.marginLeftInput.value = settings.margins.left.toString();
26121
+ }
26122
+ if (this.pageSizeSelect) {
26123
+ this.pageSizeSelect.value = settings.pageSize;
26124
+ }
26125
+ if (this.orientationSelect) {
26126
+ this.orientationSelect.value = settings.pageOrientation;
26127
+ }
26128
+ }
26129
+ catch (error) {
26130
+ console.error('Failed to load document settings:', error);
26131
+ }
26132
+ }
26133
+ applyMargins() {
26134
+ if (!this.editor)
26135
+ return;
26136
+ const margins = {
26137
+ top: parseFloat(this.marginTopInput?.value || '20'),
26138
+ right: parseFloat(this.marginRightInput?.value || '20'),
26139
+ bottom: parseFloat(this.marginBottomInput?.value || '20'),
26140
+ left: parseFloat(this.marginLeftInput?.value || '20')
26141
+ };
26142
+ try {
26143
+ this.editor.updateDocumentSettings({ margins });
26144
+ }
26145
+ catch (error) {
26146
+ console.error('Failed to update margins:', error);
26147
+ }
26148
+ }
26149
+ applyPageSettings() {
26150
+ if (!this.editor)
26151
+ return;
26152
+ const settings = {};
26153
+ if (this.pageSizeSelect) {
26154
+ settings.pageSize = this.pageSizeSelect.value;
26155
+ }
26156
+ if (this.orientationSelect) {
26157
+ settings.pageOrientation = this.orientationSelect.value;
26158
+ }
26159
+ try {
26160
+ this.editor.updateDocumentSettings(settings);
26161
+ }
26162
+ catch (error) {
26163
+ console.error('Failed to update page settings:', error);
26164
+ }
26165
+ }
26166
+ /**
26167
+ * Update the pane from current editor state.
26168
+ */
26169
+ update() {
26170
+ this.loadSettings();
26171
+ }
26172
+ }
26173
+
26174
+ /**
26175
+ * MergeDataPane - JSON data input for mail merge/substitution.
26176
+ *
26177
+ * Uses the PCEditor public API:
26178
+ * - editor.applyMergeData()
26179
+ */
26180
+ class MergeDataPane extends BasePane {
26181
+ constructor(id = 'merge-data', options = {}) {
26182
+ super(id, { className: 'pc-pane-merge-data', ...options });
26183
+ this.textarea = null;
26184
+ this.errorHint = null;
26185
+ this.initialData = options.initialData;
26186
+ this.placeholder = options.placeholder || '{"customerName": "John Doe", "orderNumber": "12345"}';
26187
+ this.rows = options.rows ?? 10;
26188
+ this.onApply = options.onApply;
26189
+ }
26190
+ createContent() {
26191
+ const container = document.createElement('div');
26192
+ // Textarea for JSON
26193
+ const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
26194
+ container.appendChild(textareaGroup);
26195
+ // Error hint (hidden by default)
26196
+ this.errorHint = this.createHint('');
26197
+ this.errorHint.style.display = 'none';
26198
+ this.errorHint.style.color = '#dc3545';
26199
+ container.appendChild(this.errorHint);
26200
+ // Apply button
26201
+ const applyBtn = this.createButton('Apply Merge Data', { variant: 'primary' });
26202
+ this.addButtonListener(applyBtn, () => this.applyMergeData());
26203
+ container.appendChild(applyBtn);
26204
+ return container;
26205
+ }
26206
+ createTextarea() {
26207
+ this.textarea = document.createElement('textarea');
26208
+ this.textarea.className = 'pc-pane-textarea pc-pane-merge-data-input';
26209
+ this.textarea.rows = this.rows;
26210
+ this.textarea.placeholder = this.placeholder;
26211
+ this.textarea.spellcheck = false;
26212
+ if (this.initialData) {
26213
+ this.textarea.value = JSON.stringify(this.initialData, null, 2);
26214
+ }
26215
+ // Clear error on input
26216
+ this.textarea.addEventListener('input', () => {
26217
+ if (this.errorHint) {
26218
+ this.errorHint.style.display = 'none';
26219
+ }
26220
+ });
26221
+ return this.textarea;
26222
+ }
26223
+ applyMergeData() {
26224
+ if (!this.editor || !this.textarea)
26225
+ return;
26226
+ try {
26227
+ const mergeData = JSON.parse(this.textarea.value);
26228
+ this.editor.applyMergeData(mergeData);
26229
+ if (this.errorHint) {
26230
+ this.errorHint.style.display = 'none';
26231
+ }
26232
+ this.onApply?.(true);
26233
+ }
26234
+ catch (error) {
26235
+ const err = error instanceof Error ? error : new Error(String(error));
26236
+ if (this.errorHint) {
26237
+ if (error instanceof SyntaxError) {
26238
+ this.errorHint.textContent = 'Invalid JSON syntax';
26239
+ }
26240
+ else {
26241
+ this.errorHint.textContent = err.message;
26242
+ }
26243
+ this.errorHint.style.display = 'block';
26244
+ }
26245
+ this.onApply?.(false, err);
26246
+ }
26247
+ }
26248
+ /**
26249
+ * Get the current JSON data from the textarea.
26250
+ * Returns null if the JSON is invalid.
26251
+ */
26252
+ getData() {
26253
+ if (!this.textarea)
26254
+ return null;
26255
+ try {
26256
+ return JSON.parse(this.textarea.value);
26257
+ }
26258
+ catch {
26259
+ return null;
26260
+ }
26261
+ }
26262
+ /**
26263
+ * Set the JSON data in the textarea.
26264
+ */
26265
+ setData(data) {
26266
+ if (this.textarea) {
26267
+ this.textarea.value = JSON.stringify(data, null, 2);
26268
+ if (this.errorHint) {
26269
+ this.errorHint.style.display = 'none';
26270
+ }
26271
+ }
26272
+ }
26273
+ /**
26274
+ * Update the pane (no-op for MergeDataPane as it doesn't track editor state).
26275
+ */
26276
+ update() {
26277
+ // MergeDataPane doesn't need to update from editor state
26278
+ }
26279
+ }
26280
+
26281
+ /**
26282
+ * FormattingPane - Text formatting controls.
26283
+ *
26284
+ * Controls:
26285
+ * - Bold/Italic toggles
26286
+ * - Alignment buttons (left, center, right, justify)
26287
+ * - List buttons (bullet, numbered, indent, outdent)
26288
+ * - Font family/size dropdowns
26289
+ * - Text color and highlight color pickers
26290
+ *
26291
+ * Uses the PCEditor public API:
26292
+ * - editor.getUnifiedFormattingAtCursor()
26293
+ * - editor.applyFormattingWithFallback()
26294
+ * - editor.setPendingFormatting()
26295
+ * - editor.getSavedOrCurrentSelection()
26296
+ * - editor.getUnifiedAlignmentAtCursor()
26297
+ * - editor.setUnifiedAlignment()
26298
+ * - editor.toggleBulletList()
26299
+ * - editor.toggleNumberedList()
26300
+ * - editor.indentParagraph()
26301
+ * - editor.outdentParagraph()
26302
+ * - editor.getListFormatting()
26303
+ */
26304
+ const DEFAULT_FONT_FAMILIES = [
26305
+ 'Arial',
26306
+ 'Times New Roman',
26307
+ 'Georgia',
26308
+ 'Verdana',
26309
+ 'Courier New'
26310
+ ];
26311
+ const DEFAULT_FONT_SIZES = [10, 12, 14, 16, 18, 20, 24, 28, 32, 36];
26312
+ class FormattingPane extends BasePane {
26313
+ constructor(id = 'formatting', options = {}) {
26314
+ super(id, { className: 'pc-pane-formatting', ...options });
26315
+ // Style toggles
26316
+ this.boldBtn = null;
26317
+ this.italicBtn = null;
26318
+ // Alignment buttons
26319
+ this.alignLeftBtn = null;
26320
+ this.alignCenterBtn = null;
26321
+ this.alignRightBtn = null;
26322
+ this.alignJustifyBtn = null;
26323
+ // List buttons
26324
+ this.bulletListBtn = null;
26325
+ this.numberedListBtn = null;
26326
+ this.indentBtn = null;
26327
+ this.outdentBtn = null;
26328
+ // Font controls
26329
+ this.fontFamilySelect = null;
26330
+ this.fontSizeSelect = null;
26331
+ this.colorInput = null;
26332
+ this.highlightInput = null;
26333
+ this.fontFamilies = options.fontFamilies ?? DEFAULT_FONT_FAMILIES;
26334
+ this.fontSizes = options.fontSizes ?? DEFAULT_FONT_SIZES;
26335
+ }
26336
+ attach(options) {
26337
+ super.attach(options);
26338
+ if (this.editor) {
26339
+ // Update on cursor/selection changes
26340
+ const updateHandler = () => this.updateFromEditor();
26341
+ this.editor.on('cursor-changed', updateHandler);
26342
+ this.editor.on('selection-changed', updateHandler);
26343
+ this.editor.on('text-changed', updateHandler);
26344
+ this.editor.on('formatting-changed', updateHandler);
26345
+ this.eventCleanup.push(() => {
26346
+ this.editor?.off('cursor-changed', updateHandler);
26347
+ this.editor?.off('selection-changed', updateHandler);
26348
+ this.editor?.off('text-changed', updateHandler);
26349
+ this.editor?.off('formatting-changed', updateHandler);
26350
+ });
26351
+ // Initial update
26352
+ this.updateFromEditor();
26353
+ }
26354
+ }
26355
+ createContent() {
26356
+ const container = document.createElement('div');
26357
+ // Style section (Bold, Italic)
26358
+ const styleSection = this.createSection('Style');
26359
+ const styleGroup = this.createButtonGroup();
26360
+ this.boldBtn = this.createButton('B');
26361
+ this.boldBtn.title = 'Bold';
26362
+ this.boldBtn.style.fontWeight = 'bold';
26363
+ this.addButtonListener(this.boldBtn, () => this.toggleBold());
26364
+ this.italicBtn = this.createButton('I');
26365
+ this.italicBtn.title = 'Italic';
26366
+ this.italicBtn.style.fontStyle = 'italic';
26367
+ this.addButtonListener(this.italicBtn, () => this.toggleItalic());
26368
+ styleGroup.appendChild(this.boldBtn);
26369
+ styleGroup.appendChild(this.italicBtn);
26370
+ styleSection.appendChild(styleGroup);
26371
+ container.appendChild(styleSection);
26372
+ // Alignment section
26373
+ const alignSection = this.createSection('Alignment');
26374
+ const alignGroup = this.createButtonGroup();
26375
+ this.alignLeftBtn = this.createButton('');
26376
+ this.alignLeftBtn.title = 'Align Left';
26377
+ this.alignLeftBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-left');
26378
+ this.addButtonListener(this.alignLeftBtn, () => this.setAlignment('left'));
26379
+ this.alignCenterBtn = this.createButton('');
26380
+ this.alignCenterBtn.title = 'Center';
26381
+ this.alignCenterBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-center');
26382
+ this.addButtonListener(this.alignCenterBtn, () => this.setAlignment('center'));
26383
+ this.alignRightBtn = this.createButton('');
26384
+ this.alignRightBtn.title = 'Align Right';
26385
+ this.alignRightBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-right');
26386
+ this.addButtonListener(this.alignRightBtn, () => this.setAlignment('right'));
26387
+ this.alignJustifyBtn = this.createButton('');
26388
+ this.alignJustifyBtn.title = 'Justify';
26389
+ this.alignJustifyBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-justify');
26390
+ this.addButtonListener(this.alignJustifyBtn, () => this.setAlignment('justify'));
26391
+ alignGroup.appendChild(this.alignLeftBtn);
26392
+ alignGroup.appendChild(this.alignCenterBtn);
26393
+ alignGroup.appendChild(this.alignRightBtn);
26394
+ alignGroup.appendChild(this.alignJustifyBtn);
26395
+ alignSection.appendChild(alignGroup);
26396
+ container.appendChild(alignSection);
26397
+ // Lists section
26398
+ const listsSection = this.createSection('Lists');
26399
+ const listsGroup = this.createButtonGroup();
26400
+ this.bulletListBtn = this.createButton('\u2022'); // •
26401
+ this.bulletListBtn.title = 'Bullet List';
26402
+ this.addButtonListener(this.bulletListBtn, () => this.toggleBulletList());
26403
+ this.numberedListBtn = this.createButton('1.');
26404
+ this.numberedListBtn.title = 'Numbered List';
26405
+ this.addButtonListener(this.numberedListBtn, () => this.toggleNumberedList());
26406
+ this.indentBtn = this.createButton('\u2192'); // →
26407
+ this.indentBtn.title = 'Increase Indent';
26408
+ this.addButtonListener(this.indentBtn, () => this.indent());
26409
+ this.outdentBtn = this.createButton('\u2190'); // ←
26410
+ this.outdentBtn.title = 'Decrease Indent';
26411
+ this.addButtonListener(this.outdentBtn, () => this.outdent());
26412
+ listsGroup.appendChild(this.bulletListBtn);
26413
+ listsGroup.appendChild(this.numberedListBtn);
26414
+ listsGroup.appendChild(this.indentBtn);
26415
+ listsGroup.appendChild(this.outdentBtn);
26416
+ listsSection.appendChild(listsGroup);
26417
+ container.appendChild(listsSection);
26418
+ // Font section
26419
+ const fontSection = this.createSection('Font');
26420
+ this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26421
+ this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26422
+ fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
26423
+ this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26424
+ this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26425
+ fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
26426
+ container.appendChild(fontSection);
26427
+ // Color section
26428
+ const colorSection = this.createSection('Color');
26429
+ const colorRow = this.createRow();
26430
+ const colorGroup = document.createElement('div');
26431
+ this.colorInput = this.createColorInput('#000000');
26432
+ this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26433
+ colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26434
+ colorRow.appendChild(colorGroup);
26435
+ const highlightGroup = document.createElement('div');
26436
+ this.highlightInput = this.createColorInput('#ffff00');
26437
+ this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26438
+ const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
26439
+ const clearHighlightBtn = this.createButton('Clear');
26440
+ clearHighlightBtn.className = 'pc-pane-button';
26441
+ clearHighlightBtn.style.marginLeft = '4px';
26442
+ this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26443
+ highlightForm.appendChild(clearHighlightBtn);
26444
+ highlightGroup.appendChild(highlightForm);
26445
+ colorRow.appendChild(highlightGroup);
26446
+ colorSection.appendChild(colorRow);
26447
+ container.appendChild(colorSection);
26448
+ return container;
26449
+ }
26450
+ updateFromEditor() {
26451
+ if (!this.editor)
26452
+ return;
26453
+ // Get formatting at cursor
26454
+ const formatting = this.editor.getUnifiedFormattingAtCursor();
26455
+ if (formatting) {
26456
+ // Update bold button
26457
+ this.boldBtn?.classList.toggle('pc-pane-button--active', formatting.fontWeight === 'bold');
26458
+ // Update italic button
26459
+ this.italicBtn?.classList.toggle('pc-pane-button--active', formatting.fontStyle === 'italic');
26460
+ // Update font family
26461
+ if (this.fontFamilySelect && formatting.fontFamily) {
26462
+ this.fontFamilySelect.value = formatting.fontFamily;
26463
+ }
26464
+ // Update font size
26465
+ if (this.fontSizeSelect && formatting.fontSize) {
26466
+ this.fontSizeSelect.value = formatting.fontSize.toString();
26467
+ }
26468
+ // Update color
26469
+ if (this.colorInput && formatting.color) {
26470
+ this.colorInput.value = formatting.color;
26471
+ }
26472
+ // Update highlight
26473
+ if (this.highlightInput && formatting.backgroundColor) {
26474
+ this.highlightInput.value = formatting.backgroundColor;
26475
+ }
26476
+ }
26477
+ // Update alignment buttons
26478
+ const alignment = this.editor.getUnifiedAlignmentAtCursor();
26479
+ this.updateAlignmentButtons(alignment);
26480
+ // Update list buttons
26481
+ this.updateListButtons();
26482
+ }
26483
+ updateAlignmentButtons(alignment) {
26484
+ const buttons = [
26485
+ { btn: this.alignLeftBtn, align: 'left' },
26486
+ { btn: this.alignCenterBtn, align: 'center' },
26487
+ { btn: this.alignRightBtn, align: 'right' },
26488
+ { btn: this.alignJustifyBtn, align: 'justify' }
26489
+ ];
26490
+ for (const { btn, align } of buttons) {
26491
+ btn?.classList.toggle('pc-pane-button--active', align === alignment);
26492
+ }
26493
+ }
26494
+ updateListButtons() {
26495
+ if (!this.editor)
26496
+ return;
26497
+ try {
26498
+ const listFormatting = this.editor.getListFormatting();
26499
+ if (listFormatting) {
26500
+ this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26501
+ this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26502
+ }
26503
+ }
26504
+ catch {
26505
+ // No text editing active
26506
+ }
26507
+ }
26508
+ getSelection() {
26509
+ if (!this.editor)
26510
+ return null;
26511
+ return this.editor.getSavedOrCurrentSelection();
26512
+ }
26513
+ applyFormatting(formatting) {
26514
+ if (!this.editor)
26515
+ return;
26516
+ const selection = this.getSelection();
26517
+ try {
26518
+ if (selection) {
26519
+ this.editor.applyFormattingWithFallback(selection.start, selection.end, formatting);
26520
+ }
26521
+ else {
26522
+ this.editor.setPendingFormatting(formatting);
26523
+ }
26524
+ this.editor.clearSavedEditingContext();
26525
+ this.updateFromEditor();
26526
+ this.editor.enableTextInput();
26527
+ }
26528
+ catch (error) {
26529
+ console.error('Formatting error:', error);
26530
+ }
26531
+ }
26532
+ toggleBold() {
26533
+ const isActive = this.boldBtn?.classList.contains('pc-pane-button--active');
26534
+ this.applyFormatting({ fontWeight: isActive ? 'normal' : 'bold' });
26535
+ }
26536
+ toggleItalic() {
26537
+ const isActive = this.italicBtn?.classList.contains('pc-pane-button--active');
26538
+ this.applyFormatting({ fontStyle: isActive ? 'normal' : 'italic' });
26539
+ }
26540
+ applyFontFamily() {
26541
+ if (this.fontFamilySelect) {
26542
+ this.applyFormatting({ fontFamily: this.fontFamilySelect.value });
26543
+ }
26544
+ }
26545
+ applyFontSize() {
26546
+ if (this.fontSizeSelect) {
26547
+ this.applyFormatting({ fontSize: parseInt(this.fontSizeSelect.value, 10) });
26548
+ }
26549
+ }
26550
+ applyTextColor() {
26551
+ if (this.colorInput) {
26552
+ this.applyFormatting({ color: this.colorInput.value });
26553
+ }
26554
+ }
26555
+ applyHighlight() {
26556
+ if (this.highlightInput) {
26557
+ this.applyFormatting({ backgroundColor: this.highlightInput.value });
26558
+ }
26559
+ }
26560
+ clearHighlight() {
26561
+ this.applyFormatting({ backgroundColor: undefined });
26562
+ }
26563
+ setAlignment(alignment) {
26564
+ if (!this.editor)
26565
+ return;
26566
+ try {
26567
+ this.editor.setUnifiedAlignment(alignment);
26568
+ this.updateAlignmentButtons(alignment);
26569
+ }
26570
+ catch (error) {
26571
+ console.error('Alignment error:', error);
26572
+ }
26573
+ }
26574
+ toggleBulletList() {
26575
+ if (!this.editor)
26576
+ return;
26577
+ try {
26578
+ this.editor.toggleBulletList();
26579
+ this.updateListButtons();
26580
+ }
26581
+ catch (error) {
26582
+ console.error('Bullet list error:', error);
26583
+ }
26584
+ }
26585
+ toggleNumberedList() {
26586
+ if (!this.editor)
26587
+ return;
26588
+ try {
26589
+ this.editor.toggleNumberedList();
26590
+ this.updateListButtons();
26591
+ }
26592
+ catch (error) {
26593
+ console.error('Numbered list error:', error);
26594
+ }
26595
+ }
26596
+ indent() {
26597
+ if (!this.editor)
26598
+ return;
26599
+ try {
26600
+ this.editor.indentParagraph();
26601
+ this.updateListButtons();
26602
+ }
26603
+ catch (error) {
26604
+ console.error('Indent error:', error);
26605
+ }
26606
+ }
26607
+ outdent() {
26608
+ if (!this.editor)
26609
+ return;
26610
+ try {
26611
+ this.editor.outdentParagraph();
26612
+ this.updateListButtons();
26613
+ }
26614
+ catch (error) {
26615
+ console.error('Outdent error:', error);
26616
+ }
26617
+ }
26618
+ /**
26619
+ * Update the pane from current editor state.
26620
+ */
26621
+ update() {
26622
+ this.updateFromEditor();
26623
+ }
26624
+ }
26625
+
26626
+ /**
26627
+ * HyperlinkPane - Edit hyperlink URL and title.
26628
+ *
26629
+ * This pane is shown when a hyperlink is selected/cursor is in a hyperlink.
26630
+ *
26631
+ * Uses the PCEditor public API:
26632
+ * - editor.getHyperlinkAt()
26633
+ * - editor.updateHyperlink()
26634
+ * - editor.removeHyperlink()
26635
+ * - editor.getCursorPosition()
26636
+ */
26637
+ class HyperlinkPane extends BasePane {
26638
+ constructor(id = 'hyperlink', options = {}) {
26639
+ super(id, { className: 'pc-pane-hyperlink', ...options });
26640
+ this.urlInput = null;
26641
+ this.titleInput = null;
26642
+ this.rangeHint = null;
26643
+ this.currentHyperlink = null;
26644
+ this._isUpdating = false;
26645
+ this.onApply = options.onApply;
26646
+ this.onRemove = options.onRemove;
26647
+ }
26648
+ attach(options) {
26649
+ super.attach(options);
26650
+ if (this.editor) {
26651
+ // Update on cursor changes
26652
+ const updateHandler = () => this.updateFromCursor();
26653
+ this.editor.on('cursor-changed', updateHandler);
26654
+ this.editor.on('selection-changed', updateHandler);
26655
+ this.eventCleanup.push(() => {
26656
+ this.editor?.off('cursor-changed', updateHandler);
26657
+ this.editor?.off('selection-changed', updateHandler);
26658
+ });
26659
+ // Initial update
26660
+ this.updateFromCursor();
26661
+ }
26662
+ }
26663
+ createContent() {
26664
+ const container = document.createElement('div');
26665
+ // URL input
26666
+ this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26667
+ container.appendChild(this.createFormGroup('URL', this.urlInput));
26668
+ // Title input
26669
+ this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26670
+ container.appendChild(this.createFormGroup('Title', this.titleInput));
26671
+ // Apply button
26672
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26673
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26674
+ container.appendChild(applyBtn);
26675
+ // Remove button
26676
+ const removeBtn = this.createButton('Remove Link', { variant: 'danger' });
26677
+ removeBtn.style.marginTop = '0.5rem';
26678
+ this.addButtonListener(removeBtn, () => this.removeHyperlink());
26679
+ container.appendChild(removeBtn);
26680
+ // Range hint
26681
+ this.rangeHint = this.createHint('');
26682
+ container.appendChild(this.rangeHint);
26683
+ return container;
26684
+ }
26685
+ updateFromCursor() {
26686
+ if (!this.editor || this._isUpdating)
26687
+ return;
26688
+ this._isUpdating = true;
26689
+ try {
26690
+ const cursorPos = this.editor.getCursorPosition();
26691
+ const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26692
+ if (hyperlink) {
26693
+ this.showHyperlink(hyperlink);
26694
+ }
26695
+ else {
26696
+ this.hideHyperlink();
26697
+ }
26698
+ }
26699
+ finally {
26700
+ this._isUpdating = false;
26701
+ }
26702
+ }
26703
+ showHyperlink(hyperlink) {
26704
+ this.currentHyperlink = hyperlink;
26705
+ if (this.urlInput) {
26706
+ this.urlInput.value = hyperlink.url;
26707
+ }
26708
+ if (this.titleInput) {
26709
+ this.titleInput.value = hyperlink.title || '';
26710
+ }
26711
+ if (this.rangeHint) {
26712
+ this.rangeHint.textContent = `Link spans characters ${hyperlink.startIndex} to ${hyperlink.endIndex}`;
26713
+ }
26714
+ // Show the pane
26715
+ this.show();
26716
+ }
26717
+ hideHyperlink() {
26718
+ this.currentHyperlink = null;
26719
+ this.hide();
26720
+ }
26721
+ applyChanges() {
26722
+ if (!this.editor || !this.currentHyperlink)
26723
+ return;
26724
+ try {
26725
+ const url = this.urlInput?.value.trim() || '';
26726
+ const title = this.titleInput?.value.trim() || undefined;
26727
+ if (!url) {
26728
+ this.onApply?.(false, new Error('URL is required'));
26729
+ return;
26730
+ }
26731
+ this.editor.updateHyperlink(this.currentHyperlink.id, { url, title });
26732
+ // Update local reference
26733
+ this.currentHyperlink.url = url;
26734
+ this.currentHyperlink.title = title;
26735
+ this.onApply?.(true);
26736
+ }
26737
+ catch (error) {
26738
+ this.onApply?.(false, error instanceof Error ? error : new Error(String(error)));
26739
+ }
26740
+ }
26741
+ removeHyperlink() {
26742
+ if (!this.editor || !this.currentHyperlink)
26743
+ return;
26744
+ try {
26745
+ this.editor.removeHyperlink(this.currentHyperlink.id);
26746
+ this.hideHyperlink();
26747
+ this.onRemove?.(true);
26748
+ }
26749
+ catch {
26750
+ this.onRemove?.(false);
26751
+ }
26752
+ }
26753
+ /**
26754
+ * Get the currently selected hyperlink.
26755
+ */
26756
+ getCurrentHyperlink() {
26757
+ return this.currentHyperlink;
26758
+ }
26759
+ /**
26760
+ * Check if a hyperlink is currently selected.
26761
+ */
26762
+ hasHyperlink() {
26763
+ return this.currentHyperlink !== null;
26764
+ }
26765
+ /**
26766
+ * Update the pane from current editor state.
26767
+ */
26768
+ update() {
26769
+ this.updateFromCursor();
26770
+ }
26771
+ }
26772
+
26773
+ /**
26774
+ * SubstitutionFieldPane - Edit substitution field properties.
26775
+ *
26776
+ * Shows:
26777
+ * - Field name
26778
+ * - Default value
26779
+ * - Format configuration (value type, number/currency/date formats)
26780
+ *
26781
+ * Uses the PCEditor public API:
26782
+ * - editor.getFieldAt()
26783
+ * - editor.updateField()
26784
+ */
26785
+ class SubstitutionFieldPane extends BasePane {
26786
+ constructor(id = 'substitution-field', options = {}) {
26787
+ super(id, { className: 'pc-pane-substitution-field', ...options });
26788
+ this.fieldNameInput = null;
26789
+ this.fieldDefaultInput = null;
26790
+ this.valueTypeSelect = null;
26791
+ this.numberFormatSelect = null;
26792
+ this.currencyFormatSelect = null;
26793
+ this.dateFormatSelect = null;
26794
+ this.positionHint = null;
26795
+ this.numberFormatGroup = null;
26796
+ this.currencyFormatGroup = null;
26797
+ this.dateFormatGroup = null;
26798
+ this.currentField = null;
26799
+ this.onApplyCallback = options.onApply;
26800
+ }
26801
+ attach(options) {
26802
+ super.attach(options);
26803
+ if (this.editor) {
26804
+ // Listen for field selection events
26805
+ const selectionHandler = (event) => {
26806
+ if (event.type === 'field' && event.field) {
26807
+ this.showField(event.field);
26808
+ }
26809
+ else if (!event.type || event.type !== 'field') ;
26810
+ };
26811
+ this.editor.on('selection-change', selectionHandler);
26812
+ this.eventCleanup.push(() => {
26813
+ this.editor?.off('selection-change', selectionHandler);
26814
+ });
26815
+ }
26816
+ }
26817
+ createContent() {
26818
+ const container = document.createElement('div');
26819
+ // Field name input
26820
+ this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26821
+ container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
26822
+ // Default value input
26823
+ this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26824
+ container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
26825
+ // Value type select
26826
+ this.valueTypeSelect = this.createSelect([
26827
+ { value: '', label: '(None)' },
26828
+ { value: 'number', label: 'Number' },
26829
+ { value: 'currency', label: 'Currency' },
26830
+ { value: 'date', label: 'Date' }
26831
+ ]);
26832
+ this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26833
+ container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
26834
+ // Number format group
26835
+ this.numberFormatGroup = this.createSection();
26836
+ this.numberFormatGroup.style.display = 'none';
26837
+ this.numberFormatSelect = this.createSelect([
26838
+ { value: '0', label: 'Integer (0)' },
26839
+ { value: '0.00', label: 'Two decimals (0.00)' },
26840
+ { value: '0,0', label: 'Thousands separator (0,0)' },
26841
+ { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26842
+ ]);
26843
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
26844
+ container.appendChild(this.numberFormatGroup);
26845
+ // Currency format group
26846
+ this.currencyFormatGroup = this.createSection();
26847
+ this.currencyFormatGroup.style.display = 'none';
26848
+ this.currencyFormatSelect = this.createSelect([
26849
+ { value: 'USD', label: 'USD ($)' },
26850
+ { value: 'EUR', label: 'EUR' },
26851
+ { value: 'GBP', label: 'GBP' },
26852
+ { value: 'JPY', label: 'JPY' }
26853
+ ]);
26854
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
26855
+ container.appendChild(this.currencyFormatGroup);
26856
+ // Date format group
26857
+ this.dateFormatGroup = this.createSection();
26858
+ this.dateFormatGroup.style.display = 'none';
26859
+ this.dateFormatSelect = this.createSelect([
26860
+ { value: 'MMMM D, YYYY', label: 'January 1, 2026' },
26861
+ { value: 'MM/DD/YYYY', label: '01/01/2026' },
26862
+ { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26863
+ { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26864
+ ]);
26865
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
26866
+ container.appendChild(this.dateFormatGroup);
26867
+ // Apply button
26868
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26869
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26870
+ container.appendChild(applyBtn);
26871
+ // Position hint
26872
+ this.positionHint = this.createHint('');
26873
+ container.appendChild(this.positionHint);
26874
+ return container;
26875
+ }
26876
+ updateFormatGroups() {
26877
+ const valueType = this.valueTypeSelect?.value || '';
26878
+ if (this.numberFormatGroup) {
26879
+ this.numberFormatGroup.style.display = valueType === 'number' ? 'block' : 'none';
26880
+ }
26881
+ if (this.currencyFormatGroup) {
26882
+ this.currencyFormatGroup.style.display = valueType === 'currency' ? 'block' : 'none';
26883
+ }
26884
+ if (this.dateFormatGroup) {
26885
+ this.dateFormatGroup.style.display = valueType === 'date' ? 'block' : 'none';
26886
+ }
26887
+ }
26888
+ /**
26889
+ * Show the pane with the given field.
26890
+ */
26891
+ showField(field) {
26892
+ this.currentField = field;
26893
+ if (this.fieldNameInput) {
26894
+ this.fieldNameInput.value = field.fieldName;
26895
+ }
26896
+ if (this.fieldDefaultInput) {
26897
+ this.fieldDefaultInput.value = field.defaultValue || '';
26898
+ }
26899
+ if (this.positionHint) {
26900
+ this.positionHint.textContent = `Field at position ${field.textIndex}`;
26901
+ }
26902
+ // Populate format options
26903
+ if (this.valueTypeSelect) {
26904
+ this.valueTypeSelect.value = field.formatConfig?.valueType || '';
26905
+ }
26906
+ if (this.numberFormatSelect && field.formatConfig?.numberFormat) {
26907
+ this.numberFormatSelect.value = field.formatConfig.numberFormat;
26908
+ }
26909
+ if (this.currencyFormatSelect && field.formatConfig?.currencyFormat) {
26910
+ this.currencyFormatSelect.value = field.formatConfig.currencyFormat;
26911
+ }
26912
+ if (this.dateFormatSelect && field.formatConfig?.dateFormat) {
26913
+ this.dateFormatSelect.value = field.formatConfig.dateFormat;
26914
+ }
26915
+ this.updateFormatGroups();
26916
+ this.show();
26917
+ }
26918
+ /**
26919
+ * Hide the pane and clear the current field.
26920
+ */
26921
+ hideField() {
26922
+ this.currentField = null;
26923
+ this.hide();
26924
+ }
26925
+ applyChanges() {
26926
+ if (!this.editor || !this.currentField) {
26927
+ this.onApplyCallback?.(false, new Error('No field selected'));
26928
+ return;
26929
+ }
26930
+ const fieldName = this.fieldNameInput?.value.trim();
26931
+ if (!fieldName) {
26932
+ this.onApplyCallback?.(false, new Error('Field name cannot be empty'));
26933
+ return;
26934
+ }
26935
+ const updates = {};
26936
+ if (fieldName !== this.currentField.fieldName) {
26937
+ updates.fieldName = fieldName;
26938
+ }
26939
+ const defaultValue = this.fieldDefaultInput?.value || undefined;
26940
+ if (defaultValue !== this.currentField.defaultValue) {
26941
+ updates.defaultValue = defaultValue;
26942
+ }
26943
+ // Build format config
26944
+ const valueType = this.valueTypeSelect?.value;
26945
+ if (valueType) {
26946
+ const formatConfig = {
26947
+ valueType: valueType
26948
+ };
26949
+ if (valueType === 'number' && this.numberFormatSelect?.value) {
26950
+ formatConfig.numberFormat = this.numberFormatSelect.value;
26951
+ }
26952
+ else if (valueType === 'currency' && this.currencyFormatSelect?.value) {
26953
+ formatConfig.currencyFormat = this.currencyFormatSelect.value;
26954
+ }
26955
+ else if (valueType === 'date' && this.dateFormatSelect?.value) {
26956
+ formatConfig.dateFormat = this.dateFormatSelect.value;
26957
+ }
26958
+ updates.formatConfig = formatConfig;
26959
+ }
26960
+ else if (this.currentField.formatConfig) {
26961
+ updates.formatConfig = undefined;
26962
+ }
26963
+ if (Object.keys(updates).length === 0) {
26964
+ return; // No changes
26965
+ }
26966
+ try {
26967
+ const success = this.editor.updateField(this.currentField.textIndex, updates);
26968
+ if (success) {
26969
+ // Update the current field reference
26970
+ this.currentField = this.editor.getFieldAt(this.currentField.textIndex) || null;
26971
+ this.onApplyCallback?.(true);
26972
+ }
26973
+ else {
26974
+ this.onApplyCallback?.(false, new Error('Failed to update field'));
26975
+ }
26976
+ }
26977
+ catch (error) {
26978
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
26979
+ }
26980
+ }
26981
+ /**
26982
+ * Get the currently selected field.
26983
+ */
26984
+ getCurrentField() {
26985
+ return this.currentField;
26986
+ }
26987
+ /**
26988
+ * Check if a field is currently selected.
26989
+ */
26990
+ hasField() {
26991
+ return this.currentField !== null;
26992
+ }
26993
+ /**
26994
+ * Update the pane from current editor state.
26995
+ */
26996
+ update() {
26997
+ // Field pane doesn't auto-update - it's driven by selection events
26998
+ }
26999
+ }
27000
+
27001
+ /**
27002
+ * RepeatingSectionPane - Edit repeating section (loop) properties.
27003
+ *
27004
+ * Shows:
27005
+ * - Field path (array property in merge data)
27006
+ * - Position information
27007
+ *
27008
+ * Uses the PCEditor public API:
27009
+ * - editor.getRepeatingSection()
27010
+ * - editor.updateRepeatingSectionFieldPath()
27011
+ * - editor.removeRepeatingSection()
27012
+ */
27013
+ class RepeatingSectionPane extends BasePane {
27014
+ constructor(id = 'repeating-section', options = {}) {
27015
+ super(id, { className: 'pc-pane-repeating-section', ...options });
27016
+ this.fieldPathInput = null;
27017
+ this.positionHint = null;
27018
+ this.currentSection = null;
27019
+ this.onApplyCallback = options.onApply;
27020
+ this.onRemoveCallback = options.onRemove;
27021
+ }
27022
+ attach(options) {
27023
+ super.attach(options);
27024
+ if (this.editor) {
27025
+ // Listen for repeating section selection
27026
+ const selectionHandler = (event) => {
27027
+ if (event.type === 'repeating-section' && event.sectionId) {
27028
+ const section = this.editor?.getRepeatingSection(event.sectionId);
27029
+ if (section) {
27030
+ this.showSection(section);
27031
+ }
27032
+ }
27033
+ };
27034
+ const removedHandler = () => {
27035
+ this.hideSection();
27036
+ };
27037
+ this.editor.on('selection-change', selectionHandler);
27038
+ this.editor.on('repeating-section-removed', removedHandler);
27039
+ this.eventCleanup.push(() => {
27040
+ this.editor?.off('selection-change', selectionHandler);
27041
+ this.editor?.off('repeating-section-removed', removedHandler);
27042
+ });
27043
+ }
27044
+ }
27045
+ createContent() {
27046
+ const container = document.createElement('div');
27047
+ // Field path input
27048
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27049
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
27050
+ hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
27051
+ }));
27052
+ // Apply button
27053
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27054
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27055
+ container.appendChild(applyBtn);
27056
+ // Remove button
27057
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
27058
+ removeBtn.style.marginTop = '0.5rem';
27059
+ this.addButtonListener(removeBtn, () => this.removeSection());
27060
+ container.appendChild(removeBtn);
27061
+ // Position hint
27062
+ this.positionHint = this.createHint('');
27063
+ container.appendChild(this.positionHint);
27064
+ return container;
27065
+ }
27066
+ /**
27067
+ * Show the pane with the given section.
27068
+ */
27069
+ showSection(section) {
27070
+ this.currentSection = section;
27071
+ if (this.fieldPathInput) {
27072
+ this.fieldPathInput.value = section.fieldPath;
27073
+ }
27074
+ if (this.positionHint) {
27075
+ this.positionHint.textContent = `Loop from position ${section.startIndex} to ${section.endIndex}`;
27076
+ }
27077
+ this.show();
27078
+ }
27079
+ /**
27080
+ * Hide the pane and clear the current section.
27081
+ */
27082
+ hideSection() {
27083
+ this.currentSection = null;
27084
+ this.hide();
27085
+ }
27086
+ applyChanges() {
27087
+ if (!this.editor || !this.currentSection) {
27088
+ this.onApplyCallback?.(false, new Error('No section selected'));
27089
+ return;
27090
+ }
27091
+ const fieldPath = this.fieldPathInput?.value.trim();
27092
+ if (!fieldPath) {
27093
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
27094
+ return;
27095
+ }
27096
+ if (fieldPath === this.currentSection.fieldPath) {
27097
+ return; // No changes
27098
+ }
27099
+ try {
27100
+ const success = this.editor.updateRepeatingSectionFieldPath(this.currentSection.id, fieldPath);
27101
+ if (success) {
27102
+ // Update the current section reference
27103
+ this.currentSection = this.editor.getRepeatingSection(this.currentSection.id) || null;
27104
+ if (this.currentSection) {
27105
+ this.showSection(this.currentSection);
27106
+ }
27107
+ this.onApplyCallback?.(true);
27108
+ }
27109
+ else {
27110
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
27111
+ }
27112
+ }
27113
+ catch (error) {
27114
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27115
+ }
27116
+ }
27117
+ removeSection() {
27118
+ if (!this.editor || !this.currentSection)
27119
+ return;
27120
+ try {
27121
+ this.editor.removeRepeatingSection(this.currentSection.id);
27122
+ this.hideSection();
27123
+ this.onRemoveCallback?.(true);
27124
+ }
27125
+ catch {
27126
+ this.onRemoveCallback?.(false);
27127
+ }
27128
+ }
27129
+ /**
27130
+ * Get the currently selected section.
27131
+ */
27132
+ getCurrentSection() {
27133
+ return this.currentSection;
27134
+ }
27135
+ /**
27136
+ * Check if a section is currently selected.
27137
+ */
27138
+ hasSection() {
27139
+ return this.currentSection !== null;
27140
+ }
27141
+ /**
27142
+ * Update the pane from current editor state.
27143
+ */
27144
+ update() {
27145
+ // Section pane doesn't auto-update - it's driven by selection events
27146
+ }
27147
+ }
27148
+
27149
+ /**
27150
+ * TableRowLoopPane - Edit table row loop properties.
27151
+ *
27152
+ * Shows:
27153
+ * - Field path (array property in merge data)
27154
+ * - Row range information
27155
+ *
27156
+ * Uses the TableObject API:
27157
+ * - table.getRowLoop()
27158
+ * - table.updateRowLoopFieldPath()
27159
+ * - table.removeRowLoop()
27160
+ */
27161
+ class TableRowLoopPane extends BasePane {
27162
+ constructor(id = 'table-row-loop', options = {}) {
27163
+ super(id, { className: 'pc-pane-table-row-loop', ...options });
27164
+ this.fieldPathInput = null;
27165
+ this.rangeHint = null;
27166
+ this.currentLoop = null;
27167
+ this.currentTable = null;
27168
+ this.onApplyCallback = options.onApply;
27169
+ this.onRemoveCallback = options.onRemove;
27170
+ }
27171
+ attach(options) {
27172
+ super.attach(options);
27173
+ // Table row loop pane is typically shown manually when a table's row loop is selected
27174
+ // The consumer is responsible for calling showLoop() with the table and loop
27175
+ }
27176
+ createContent() {
27177
+ const container = document.createElement('div');
27178
+ // Field path input
27179
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27180
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
27181
+ hint: 'Path to array in merge data (e.g., "items" or "orders")'
27182
+ }));
27183
+ // Apply button
27184
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27185
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27186
+ container.appendChild(applyBtn);
27187
+ // Remove button
27188
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
27189
+ removeBtn.style.marginTop = '0.5rem';
27190
+ this.addButtonListener(removeBtn, () => this.removeLoop());
27191
+ container.appendChild(removeBtn);
27192
+ // Range hint
27193
+ this.rangeHint = this.createHint('');
27194
+ container.appendChild(this.rangeHint);
27195
+ return container;
27196
+ }
27197
+ /**
27198
+ * Show the pane with the given table and loop.
27199
+ */
27200
+ showLoop(table, loop) {
27201
+ this.currentTable = table;
27202
+ this.currentLoop = loop;
27203
+ if (this.fieldPathInput) {
27204
+ this.fieldPathInput.value = loop.fieldPath;
27205
+ }
27206
+ if (this.rangeHint) {
27207
+ this.rangeHint.textContent = `Rows ${loop.startRowIndex} - ${loop.endRowIndex}`;
27208
+ }
27209
+ this.show();
27210
+ }
27211
+ /**
27212
+ * Hide the pane and clear current loop.
27213
+ */
27214
+ hideLoop() {
27215
+ this.currentTable = null;
27216
+ this.currentLoop = null;
27217
+ this.hide();
27218
+ }
27219
+ applyChanges() {
27220
+ if (!this.currentTable || !this.currentLoop) {
27221
+ this.onApplyCallback?.(false, new Error('No loop selected'));
27222
+ return;
27223
+ }
27224
+ const fieldPath = this.fieldPathInput?.value.trim();
27225
+ if (!fieldPath) {
27226
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
27227
+ return;
27228
+ }
27229
+ if (fieldPath === this.currentLoop.fieldPath) {
27230
+ return; // No changes
27231
+ }
27232
+ try {
27233
+ const success = this.currentTable.updateRowLoopFieldPath(this.currentLoop.id, fieldPath);
27234
+ if (success) {
27235
+ // Update the current loop reference
27236
+ this.currentLoop = this.currentTable.getRowLoop(this.currentLoop.id) || null;
27237
+ if (this.currentLoop) {
27238
+ this.showLoop(this.currentTable, this.currentLoop);
27239
+ }
27240
+ this.onApplyCallback?.(true);
27241
+ }
27242
+ else {
27243
+ this.onApplyCallback?.(false, new Error('Failed to update loop'));
27244
+ }
27245
+ }
27246
+ catch (error) {
27247
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27248
+ }
27249
+ }
27250
+ removeLoop() {
27251
+ if (!this.currentTable || !this.currentLoop)
27252
+ return;
27253
+ try {
27254
+ const success = this.currentTable.removeRowLoop(this.currentLoop.id);
27255
+ if (success) {
27256
+ this.hideLoop();
27257
+ this.onRemoveCallback?.(true);
27258
+ }
27259
+ else {
27260
+ this.onRemoveCallback?.(false);
27261
+ }
27262
+ }
27263
+ catch {
27264
+ this.onRemoveCallback?.(false);
27265
+ }
27266
+ }
27267
+ /**
27268
+ * Get the currently selected loop.
27269
+ */
27270
+ getCurrentLoop() {
27271
+ return this.currentLoop;
27272
+ }
27273
+ /**
27274
+ * Get the currently selected table.
27275
+ */
27276
+ getCurrentTable() {
27277
+ return this.currentTable;
27278
+ }
27279
+ /**
27280
+ * Check if a loop is currently selected.
27281
+ */
27282
+ hasLoop() {
27283
+ return this.currentLoop !== null;
27284
+ }
27285
+ /**
27286
+ * Update the pane from current editor state.
27287
+ */
27288
+ update() {
27289
+ // Table row loop pane doesn't auto-update - it's driven by showLoop() calls
27290
+ }
27291
+ }
27292
+
27293
+ /**
27294
+ * TextBoxPane - Edit text box properties.
27295
+ *
27296
+ * Shows:
27297
+ * - Position (inline, block, relative)
27298
+ * - Relative offset (for relative positioning)
27299
+ * - Background color
27300
+ * - Border (width, color, style)
27301
+ * - Padding
27302
+ *
27303
+ * Uses the PCEditor public API:
27304
+ * - editor.getSelectedTextBox()
27305
+ * - editor.updateTextBox()
27306
+ */
27307
+ class TextBoxPane extends BasePane {
27308
+ constructor(id = 'textbox', options = {}) {
27309
+ super(id, { className: 'pc-pane-textbox', ...options });
27310
+ this.positionSelect = null;
27311
+ this.offsetGroup = null;
27312
+ this.offsetXInput = null;
27313
+ this.offsetYInput = null;
27314
+ this.bgColorInput = null;
27315
+ this.borderWidthInput = null;
27316
+ this.borderColorInput = null;
27317
+ this.borderStyleSelect = null;
27318
+ this.paddingInput = null;
27319
+ this._isUpdating = false;
27320
+ this.currentTextBox = null;
27321
+ this.onApplyCallback = options.onApply;
27322
+ }
27323
+ attach(options) {
27324
+ super.attach(options);
27325
+ if (this.editor) {
27326
+ // Listen for selection changes
27327
+ const updateHandler = () => this.updateFromSelection();
27328
+ this.editor.on('selection-change', updateHandler);
27329
+ this.editor.on('textbox-updated', updateHandler);
27330
+ this.eventCleanup.push(() => {
27331
+ this.editor?.off('selection-change', updateHandler);
27332
+ this.editor?.off('textbox-updated', updateHandler);
27333
+ });
27334
+ // Initial update
27335
+ this.updateFromSelection();
27336
+ }
27337
+ }
27338
+ createContent() {
27339
+ const container = document.createElement('div');
27340
+ // Position section
27341
+ const positionSection = this.createSection('Position');
27342
+ this.positionSelect = this.createSelect([
27343
+ { value: 'inline', label: 'Inline' },
27344
+ { value: 'block', label: 'Block' },
27345
+ { value: 'relative', label: 'Relative' }
27346
+ ], 'inline');
27347
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27348
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27349
+ // Offset group (only visible for relative positioning)
27350
+ this.offsetGroup = document.createElement('div');
27351
+ this.offsetGroup.style.display = 'none';
27352
+ const offsetRow = this.createRow();
27353
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27354
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27355
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27356
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27357
+ this.offsetGroup.appendChild(offsetRow);
27358
+ positionSection.appendChild(this.offsetGroup);
27359
+ container.appendChild(positionSection);
27360
+ // Background section
27361
+ const bgSection = this.createSection('Background');
27362
+ this.bgColorInput = this.createColorInput('#ffffff');
27363
+ bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
27364
+ container.appendChild(bgSection);
27365
+ // Border section
27366
+ const borderSection = this.createSection('Border');
27367
+ const borderRow = this.createRow();
27368
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27369
+ this.borderColorInput = this.createColorInput('#cccccc');
27370
+ borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27371
+ borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27372
+ borderSection.appendChild(borderRow);
27373
+ this.borderStyleSelect = this.createSelect([
27374
+ { value: 'solid', label: 'Solid' },
27375
+ { value: 'dashed', label: 'Dashed' },
27376
+ { value: 'dotted', label: 'Dotted' },
27377
+ { value: 'none', label: 'None' }
27378
+ ], 'solid');
27379
+ borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27380
+ container.appendChild(borderSection);
27381
+ // Padding section
27382
+ const paddingSection = this.createSection('Padding');
27383
+ this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27384
+ paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
27385
+ container.appendChild(paddingSection);
27386
+ // Apply button
27387
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27388
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27389
+ container.appendChild(applyBtn);
27390
+ return container;
27391
+ }
27392
+ updateFromSelection() {
27393
+ if (!this.editor || this._isUpdating)
27394
+ return;
27395
+ this._isUpdating = true;
27396
+ try {
27397
+ const textBox = this.editor.getSelectedTextBox?.();
27398
+ if (textBox && !textBox.editing) {
27399
+ this.showTextBox(textBox);
27400
+ }
27401
+ else {
27402
+ this.hideTextBox();
27403
+ }
27404
+ }
27405
+ finally {
27406
+ this._isUpdating = false;
27407
+ }
27408
+ }
27409
+ /**
27410
+ * Show the pane with the given text box.
27411
+ */
27412
+ showTextBox(textBox) {
27413
+ this.currentTextBox = textBox;
27414
+ // Populate position
27415
+ if (this.positionSelect) {
27416
+ this.positionSelect.value = textBox.position || 'inline';
27417
+ }
27418
+ this.updateOffsetVisibility();
27419
+ // Populate offset
27420
+ if (this.offsetXInput) {
27421
+ this.offsetXInput.value = String(textBox.relativeOffset?.x ?? 0);
27422
+ }
27423
+ if (this.offsetYInput) {
27424
+ this.offsetYInput.value = String(textBox.relativeOffset?.y ?? 0);
27425
+ }
27426
+ // Populate background
27427
+ if (this.bgColorInput) {
27428
+ this.bgColorInput.value = textBox.backgroundColor || '#ffffff';
27429
+ }
27430
+ // Populate border (use first side with non-none style)
27431
+ const border = textBox.border;
27432
+ const activeBorder = border.top.style !== 'none' ? border.top :
27433
+ border.right.style !== 'none' ? border.right :
27434
+ border.bottom.style !== 'none' ? border.bottom :
27435
+ border.left.style !== 'none' ? border.left : border.top;
27436
+ if (this.borderWidthInput) {
27437
+ this.borderWidthInput.value = String(activeBorder.width);
27438
+ }
27439
+ if (this.borderColorInput) {
27440
+ this.borderColorInput.value = activeBorder.color;
27441
+ }
27442
+ if (this.borderStyleSelect) {
27443
+ this.borderStyleSelect.value = activeBorder.style;
27444
+ }
27445
+ // Populate padding
27446
+ if (this.paddingInput) {
27447
+ this.paddingInput.value = String(textBox.padding ?? 8);
27448
+ }
27449
+ this.show();
27450
+ }
27451
+ /**
27452
+ * Hide the pane and clear current text box.
27453
+ */
27454
+ hideTextBox() {
27455
+ this.currentTextBox = null;
27456
+ this.hide();
27457
+ }
27458
+ updateOffsetVisibility() {
27459
+ if (this.offsetGroup && this.positionSelect) {
27460
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27461
+ }
27462
+ }
27463
+ applyChanges() {
27464
+ if (!this.editor || !this.currentTextBox) {
27465
+ this.onApplyCallback?.(false, new Error('No text box selected'));
27466
+ return;
27467
+ }
27468
+ const updates = {};
27469
+ // Position
27470
+ if (this.positionSelect) {
27471
+ updates.position = this.positionSelect.value;
27472
+ }
27473
+ // Relative offset
27474
+ if (this.positionSelect?.value === 'relative') {
27475
+ updates.relativeOffset = {
27476
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27477
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27478
+ };
27479
+ }
27480
+ // Background color
27481
+ if (this.bgColorInput) {
27482
+ updates.backgroundColor = this.bgColorInput.value;
27483
+ }
27484
+ // Border
27485
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27486
+ const color = this.borderColorInput?.value || '#cccccc';
27487
+ const style = (this.borderStyleSelect?.value || 'solid');
27488
+ const borderSide = { width, color, style };
27489
+ updates.border = {
27490
+ top: { ...borderSide },
27491
+ right: { ...borderSide },
27492
+ bottom: { ...borderSide },
27493
+ left: { ...borderSide }
27494
+ };
27495
+ // Padding
27496
+ if (this.paddingInput) {
27497
+ updates.padding = parseInt(this.paddingInput.value, 10);
27498
+ }
27499
+ try {
27500
+ const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27501
+ if (success) {
27502
+ this.onApplyCallback?.(true);
27503
+ }
27504
+ else {
27505
+ this.onApplyCallback?.(false, new Error('Failed to update text box'));
27506
+ }
27507
+ }
27508
+ catch (error) {
27509
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27510
+ }
27511
+ }
27512
+ /**
27513
+ * Get the currently selected text box.
27514
+ */
27515
+ getCurrentTextBox() {
27516
+ return this.currentTextBox;
27517
+ }
27518
+ /**
27519
+ * Check if a text box is currently selected.
27520
+ */
27521
+ hasTextBox() {
27522
+ return this.currentTextBox !== null;
27523
+ }
27524
+ /**
27525
+ * Update the pane from current editor state.
27526
+ */
27527
+ update() {
27528
+ this.updateFromSelection();
27529
+ }
27530
+ }
27531
+
27532
+ /**
27533
+ * ImagePane - Edit image properties.
27534
+ *
27535
+ * Shows:
27536
+ * - Position (inline, block, relative)
27537
+ * - Relative offset (for relative positioning)
27538
+ * - Fit mode (contain, cover, fill, none, tile)
27539
+ * - Resize mode (free, locked-aspect-ratio)
27540
+ * - Alt text
27541
+ * - Source file picker
27542
+ *
27543
+ * Uses the PCEditor public API:
27544
+ * - editor.getSelectedImage()
27545
+ * - editor.updateImage()
27546
+ * - editor.setImageSource()
27547
+ */
27548
+ class ImagePane extends BasePane {
27549
+ constructor(id = 'image', options = {}) {
27550
+ super(id, { className: 'pc-pane-image', ...options });
27551
+ this.positionSelect = null;
27552
+ this.offsetGroup = null;
27553
+ this.offsetXInput = null;
27554
+ this.offsetYInput = null;
27555
+ this.fitModeSelect = null;
27556
+ this.resizeModeSelect = null;
27557
+ this.altTextInput = null;
27558
+ this.fileInput = null;
27559
+ this.currentImage = null;
27560
+ this._isUpdating = false;
27561
+ this.maxImageWidth = options.maxImageWidth ?? 400;
27562
+ this.maxImageHeight = options.maxImageHeight ?? 400;
27563
+ this.onApplyCallback = options.onApply;
27564
+ }
27565
+ attach(options) {
27566
+ super.attach(options);
27567
+ if (this.editor) {
27568
+ // Listen for selection changes
27569
+ const updateHandler = () => this.updateFromSelection();
27570
+ this.editor.on('selection-change', updateHandler);
27571
+ this.editor.on('image-updated', updateHandler);
27572
+ this.eventCleanup.push(() => {
27573
+ this.editor?.off('selection-change', updateHandler);
27574
+ this.editor?.off('image-updated', updateHandler);
27575
+ });
27576
+ // Initial update
27577
+ this.updateFromSelection();
27578
+ }
27579
+ }
27580
+ createContent() {
27581
+ const container = document.createElement('div');
27582
+ // Position section
27583
+ const positionSection = this.createSection('Position');
27584
+ this.positionSelect = this.createSelect([
27585
+ { value: 'inline', label: 'Inline' },
27586
+ { value: 'block', label: 'Block' },
27587
+ { value: 'relative', label: 'Relative' }
27588
+ ], 'inline');
27589
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27590
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27591
+ // Offset group (only visible for relative positioning)
27592
+ this.offsetGroup = document.createElement('div');
27593
+ this.offsetGroup.style.display = 'none';
27594
+ const offsetRow = this.createRow();
27595
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27596
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27597
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27598
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27599
+ this.offsetGroup.appendChild(offsetRow);
27600
+ positionSection.appendChild(this.offsetGroup);
27601
+ container.appendChild(positionSection);
27602
+ // Fit mode section
27603
+ const fitSection = this.createSection('Display');
27604
+ this.fitModeSelect = this.createSelect([
27605
+ { value: 'contain', label: 'Contain' },
27606
+ { value: 'cover', label: 'Cover' },
27607
+ { value: 'fill', label: 'Fill' },
27608
+ { value: 'none', label: 'None (original size)' },
27609
+ { value: 'tile', label: 'Tile' }
27610
+ ], 'contain');
27611
+ fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
27612
+ this.resizeModeSelect = this.createSelect([
27613
+ { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27614
+ { value: 'free', label: 'Free Resize' }
27615
+ ], 'locked-aspect-ratio');
27616
+ fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27617
+ container.appendChild(fitSection);
27618
+ // Alt text section
27619
+ const altSection = this.createSection('Accessibility');
27620
+ this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27621
+ altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27622
+ container.appendChild(altSection);
27623
+ // Source section
27624
+ const sourceSection = this.createSection('Source');
27625
+ this.fileInput = document.createElement('input');
27626
+ this.fileInput.type = 'file';
27627
+ this.fileInput.accept = 'image/*';
27628
+ this.fileInput.style.display = 'none';
27629
+ this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27630
+ sourceSection.appendChild(this.fileInput);
27631
+ const changeSourceBtn = this.createButton('Change Image...');
27632
+ this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27633
+ sourceSection.appendChild(changeSourceBtn);
27634
+ container.appendChild(sourceSection);
27635
+ // Apply button
27636
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27637
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27638
+ container.appendChild(applyBtn);
27639
+ return container;
27640
+ }
27641
+ updateFromSelection() {
27642
+ if (!this.editor || this._isUpdating)
27643
+ return;
27644
+ this._isUpdating = true;
27645
+ try {
27646
+ const image = this.editor.getSelectedImage?.();
27647
+ if (image) {
27648
+ this.showImage(image);
27649
+ }
27650
+ else {
27651
+ this.hideImage();
27652
+ }
27653
+ }
27654
+ finally {
27655
+ this._isUpdating = false;
27656
+ }
27657
+ }
27658
+ /**
27659
+ * Show the pane with the given image.
27660
+ */
27661
+ showImage(image) {
27662
+ this.currentImage = image;
27663
+ // Populate position
27664
+ if (this.positionSelect) {
27665
+ this.positionSelect.value = image.position || 'inline';
27666
+ }
27667
+ this.updateOffsetVisibility();
27668
+ // Populate offset
27669
+ if (this.offsetXInput) {
27670
+ this.offsetXInput.value = String(image.relativeOffset?.x ?? 0);
27671
+ }
27672
+ if (this.offsetYInput) {
27673
+ this.offsetYInput.value = String(image.relativeOffset?.y ?? 0);
27674
+ }
27675
+ // Populate fit mode
27676
+ if (this.fitModeSelect) {
27677
+ this.fitModeSelect.value = image.fit || 'contain';
27678
+ }
27679
+ // Populate resize mode
27680
+ if (this.resizeModeSelect) {
27681
+ this.resizeModeSelect.value = image.resizeMode || 'locked-aspect-ratio';
27682
+ }
27683
+ // Populate alt text
27684
+ if (this.altTextInput) {
27685
+ this.altTextInput.value = image.alt || '';
27686
+ }
27687
+ this.show();
27688
+ }
27689
+ /**
27690
+ * Hide the pane and clear current image.
27691
+ */
27692
+ hideImage() {
27693
+ this.currentImage = null;
27694
+ this.hide();
27695
+ }
27696
+ updateOffsetVisibility() {
27697
+ if (this.offsetGroup && this.positionSelect) {
27698
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27699
+ }
27700
+ }
27701
+ handleFileChange(event) {
27702
+ if (!this.editor || !this.currentImage)
27703
+ return;
27704
+ const input = event.target;
27705
+ const file = input.files?.[0];
27706
+ if (!file)
27707
+ return;
27708
+ const reader = new FileReader();
27709
+ reader.onload = (e) => {
27710
+ const dataUrl = e.target?.result;
27711
+ if (dataUrl && this.currentImage && this.editor) {
27712
+ this.editor.setImageSource(this.currentImage.id, dataUrl, {
27713
+ maxWidth: this.maxImageWidth,
27714
+ maxHeight: this.maxImageHeight
27715
+ });
27716
+ }
27717
+ };
27718
+ reader.readAsDataURL(file);
27719
+ // Reset file input so the same file can be selected again
27720
+ input.value = '';
27721
+ }
27722
+ applyChanges() {
27723
+ if (!this.editor || !this.currentImage) {
27724
+ this.onApplyCallback?.(false, new Error('No image selected'));
27725
+ return;
27726
+ }
27727
+ const updates = {};
27728
+ // Position
27729
+ if (this.positionSelect) {
27730
+ updates.position = this.positionSelect.value;
27731
+ }
27732
+ // Relative offset
27733
+ if (this.positionSelect?.value === 'relative') {
27734
+ updates.relativeOffset = {
27735
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27736
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27737
+ };
27738
+ }
27739
+ // Fit mode
27740
+ if (this.fitModeSelect) {
27741
+ updates.fit = this.fitModeSelect.value;
27742
+ }
27743
+ // Resize mode
27744
+ if (this.resizeModeSelect) {
27745
+ updates.resizeMode = this.resizeModeSelect.value;
27746
+ }
27747
+ // Alt text
27748
+ if (this.altTextInput) {
27749
+ updates.alt = this.altTextInput.value;
27750
+ }
27751
+ try {
27752
+ const success = this.editor.updateImage(this.currentImage.id, updates);
27753
+ if (success) {
27754
+ this.onApplyCallback?.(true);
27755
+ }
27756
+ else {
27757
+ this.onApplyCallback?.(false, new Error('Failed to update image'));
27758
+ }
27759
+ }
27760
+ catch (error) {
27761
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27762
+ }
27763
+ }
27764
+ /**
27765
+ * Get the currently selected image.
27766
+ */
27767
+ getCurrentImage() {
27768
+ return this.currentImage;
27769
+ }
27770
+ /**
27771
+ * Check if an image is currently selected.
27772
+ */
27773
+ hasImage() {
27774
+ return this.currentImage !== null;
27775
+ }
27776
+ /**
27777
+ * Update the pane from current editor state.
27778
+ */
27779
+ update() {
27780
+ this.updateFromSelection();
27781
+ }
27782
+ }
27783
+
27784
+ /**
27785
+ * TablePane - Edit table properties.
27786
+ *
27787
+ * Shows:
27788
+ * - Table structure (row/column count)
27789
+ * - Row/column insertion/removal
27790
+ * - Header rows/columns
27791
+ * - Default cell padding and border color
27792
+ * - Cell-specific formatting (background, borders)
27793
+ *
27794
+ * Uses the PCEditor public API:
27795
+ * - editor.getFocusedTable()
27796
+ * - editor.tableInsertRow()
27797
+ * - editor.tableRemoveRow()
27798
+ * - editor.tableInsertColumn()
27799
+ * - editor.tableRemoveColumn()
27800
+ *
27801
+ * And TableObject methods:
27802
+ * - table.setHeaderRowCount()
27803
+ * - table.setHeaderColumnCount()
27804
+ * - table.getCell()
27805
+ * - table.getCellsInRange()
27806
+ */
27807
+ class TablePane extends BasePane {
27808
+ constructor(id = 'table', options = {}) {
27809
+ super(id, { className: 'pc-pane-table', ...options });
27810
+ // Structure info
27811
+ this.rowCountDisplay = null;
27812
+ this.colCountDisplay = null;
27813
+ this.cellSelectionDisplay = null;
27814
+ // Header controls
27815
+ this.headerRowInput = null;
27816
+ this.headerColInput = null;
27817
+ // Default controls
27818
+ this.defaultPaddingInput = null;
27819
+ this.defaultBorderColorInput = null;
27820
+ // Row loop controls
27821
+ this.loopFieldInput = null;
27822
+ // Cell formatting controls
27823
+ this.cellBgColorInput = null;
27824
+ this.borderTopCheck = null;
27825
+ this.borderRightCheck = null;
27826
+ this.borderBottomCheck = null;
27827
+ this.borderLeftCheck = null;
27828
+ this.borderWidthInput = null;
27829
+ this.borderColorInput = null;
27830
+ this.borderStyleSelect = null;
27831
+ this.currentTable = null;
27832
+ this._isUpdating = false;
27833
+ this.onApplyCallback = options.onApply;
27834
+ }
27835
+ attach(options) {
27836
+ super.attach(options);
27837
+ if (this.editor) {
27838
+ // Listen for selection/focus changes
27839
+ const updateHandler = () => this.updateFromFocusedTable();
27840
+ this.editor.on('selection-change', updateHandler);
27841
+ this.editor.on('table-cell-focus', updateHandler);
27842
+ this.editor.on('table-cell-selection', updateHandler);
27843
+ this.eventCleanup.push(() => {
27844
+ this.editor?.off('selection-change', updateHandler);
27845
+ this.editor?.off('table-cell-focus', updateHandler);
27846
+ this.editor?.off('table-cell-selection', updateHandler);
27847
+ });
27848
+ // Initial update
27849
+ this.updateFromFocusedTable();
27850
+ }
27851
+ }
27852
+ createContent() {
27853
+ const container = document.createElement('div');
27854
+ // Structure section
27855
+ const structureSection = this.createSection('Structure');
27856
+ const structureInfo = document.createElement('div');
27857
+ structureInfo.className = 'pc-pane-info-list';
27858
+ this.rowCountDisplay = document.createElement('span');
27859
+ this.colCountDisplay = document.createElement('span');
27860
+ const rowInfo = document.createElement('div');
27861
+ rowInfo.className = 'pc-pane-info';
27862
+ rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27863
+ rowInfo.appendChild(this.rowCountDisplay);
27864
+ const colInfo = document.createElement('div');
27865
+ colInfo.className = 'pc-pane-info';
27866
+ colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27867
+ colInfo.appendChild(this.colCountDisplay);
27868
+ structureInfo.appendChild(rowInfo);
27869
+ structureInfo.appendChild(colInfo);
27870
+ structureSection.appendChild(structureInfo);
27871
+ // Row/column buttons
27872
+ const structureBtns = this.createButtonGroup();
27873
+ const addRowBtn = this.createButton('+ Row');
27874
+ this.addButtonListener(addRowBtn, () => this.insertRow());
27875
+ const removeRowBtn = this.createButton('- Row');
27876
+ this.addButtonListener(removeRowBtn, () => this.removeRow());
27877
+ const addColBtn = this.createButton('+ Column');
27878
+ this.addButtonListener(addColBtn, () => this.insertColumn());
27879
+ const removeColBtn = this.createButton('- Column');
27880
+ this.addButtonListener(removeColBtn, () => this.removeColumn());
27881
+ structureBtns.appendChild(addRowBtn);
27882
+ structureBtns.appendChild(removeRowBtn);
27883
+ structureBtns.appendChild(addColBtn);
27884
+ structureBtns.appendChild(removeColBtn);
27885
+ structureSection.appendChild(structureBtns);
27886
+ container.appendChild(structureSection);
27887
+ // Headers section
27888
+ const headersSection = this.createSection('Headers');
27889
+ const headerRow = this.createRow();
27890
+ this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27891
+ this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27892
+ headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27893
+ headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27894
+ headersSection.appendChild(headerRow);
27895
+ const applyHeadersBtn = this.createButton('Apply Headers');
27896
+ this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27897
+ headersSection.appendChild(applyHeadersBtn);
27898
+ 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
+ // Defaults section
27910
+ const defaultsSection = this.createSection('Defaults');
27911
+ const defaultsRow = this.createRow();
27912
+ this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
27913
+ this.defaultBorderColorInput = this.createColorInput('#cccccc');
27914
+ defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27915
+ defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27916
+ defaultsSection.appendChild(defaultsRow);
27917
+ const applyDefaultsBtn = this.createButton('Apply Defaults');
27918
+ this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27919
+ defaultsSection.appendChild(applyDefaultsBtn);
27920
+ container.appendChild(defaultsSection);
27921
+ // Cell formatting section
27922
+ const cellSection = this.createSection('Cell Formatting');
27923
+ this.cellSelectionDisplay = this.createHint('No cell selected');
27924
+ cellSection.appendChild(this.cellSelectionDisplay);
27925
+ // Background
27926
+ this.cellBgColorInput = this.createColorInput('#ffffff');
27927
+ cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
27928
+ // Border checkboxes
27929
+ const borderChecks = document.createElement('div');
27930
+ borderChecks.className = 'pc-pane-row';
27931
+ borderChecks.style.flexWrap = 'wrap';
27932
+ borderChecks.style.gap = '4px';
27933
+ this.borderTopCheck = document.createElement('input');
27934
+ this.borderTopCheck.type = 'checkbox';
27935
+ this.borderTopCheck.checked = true;
27936
+ this.borderRightCheck = document.createElement('input');
27937
+ this.borderRightCheck.type = 'checkbox';
27938
+ this.borderRightCheck.checked = true;
27939
+ this.borderBottomCheck = document.createElement('input');
27940
+ this.borderBottomCheck.type = 'checkbox';
27941
+ this.borderBottomCheck.checked = true;
27942
+ this.borderLeftCheck = document.createElement('input');
27943
+ this.borderLeftCheck.type = 'checkbox';
27944
+ this.borderLeftCheck.checked = true;
27945
+ borderChecks.appendChild(this.createCheckbox('Top', true));
27946
+ borderChecks.appendChild(this.createCheckbox('Right', true));
27947
+ borderChecks.appendChild(this.createCheckbox('Bottom', true));
27948
+ borderChecks.appendChild(this.createCheckbox('Left', true));
27949
+ // Replace created checkboxes with our tracked ones
27950
+ const checkLabels = borderChecks.querySelectorAll('label');
27951
+ if (checkLabels[0])
27952
+ checkLabels[0].replaceChild(this.borderTopCheck, checkLabels[0].querySelector('input'));
27953
+ if (checkLabels[1])
27954
+ checkLabels[1].replaceChild(this.borderRightCheck, checkLabels[1].querySelector('input'));
27955
+ if (checkLabels[2])
27956
+ checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
27957
+ if (checkLabels[3])
27958
+ checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
27959
+ cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
27960
+ // Border properties
27961
+ const borderPropsRow = this.createRow();
27962
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
27963
+ this.borderColorInput = this.createColorInput('#cccccc');
27964
+ borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27965
+ borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27966
+ cellSection.appendChild(borderPropsRow);
27967
+ this.borderStyleSelect = this.createSelect([
27968
+ { value: 'solid', label: 'Solid' },
27969
+ { value: 'dashed', label: 'Dashed' },
27970
+ { value: 'dotted', label: 'Dotted' },
27971
+ { value: 'none', label: 'None' }
27972
+ ], 'solid');
27973
+ cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27974
+ const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
27975
+ this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
27976
+ cellSection.appendChild(applyCellBtn);
27977
+ container.appendChild(cellSection);
27978
+ return container;
27979
+ }
27980
+ updateFromFocusedTable() {
27981
+ if (!this.editor || this._isUpdating)
27982
+ return;
27983
+ this._isUpdating = true;
27984
+ try {
27985
+ const table = this.editor.getFocusedTable();
27986
+ if (table) {
27987
+ this.showTable(table);
27988
+ }
27989
+ else {
27990
+ this.hideTable();
27991
+ }
27992
+ }
27993
+ finally {
27994
+ this._isUpdating = false;
27995
+ }
27996
+ }
27997
+ /**
27998
+ * Show the pane with the given table.
27999
+ */
28000
+ showTable(table) {
28001
+ this.currentTable = table;
28002
+ // Update structure info
28003
+ if (this.rowCountDisplay) {
28004
+ this.rowCountDisplay.textContent = String(table.rowCount);
28005
+ this.rowCountDisplay.className = 'pc-pane-info-value';
28006
+ }
28007
+ if (this.colCountDisplay) {
28008
+ this.colCountDisplay.textContent = String(table.columnCount);
28009
+ this.colCountDisplay.className = 'pc-pane-info-value';
28010
+ }
28011
+ // Update header counts
28012
+ if (this.headerRowInput) {
28013
+ this.headerRowInput.value = String(table.headerRowCount);
28014
+ }
28015
+ if (this.headerColInput) {
28016
+ this.headerColInput.value = String(table.headerColumnCount);
28017
+ }
28018
+ // Update defaults
28019
+ if (this.defaultPaddingInput) {
28020
+ this.defaultPaddingInput.value = String(table.defaultCellPadding);
28021
+ }
28022
+ if (this.defaultBorderColorInput) {
28023
+ this.defaultBorderColorInput.value = table.defaultBorderColor;
28024
+ }
28025
+ // Update cell selection info
28026
+ this.updateCellSelectionInfo(table);
28027
+ this.show();
28028
+ }
28029
+ /**
28030
+ * Hide the pane and clear current table.
28031
+ */
28032
+ hideTable() {
28033
+ this.currentTable = null;
28034
+ this.hide();
28035
+ }
28036
+ updateCellSelectionInfo(table) {
28037
+ if (!this.cellSelectionDisplay)
28038
+ return;
28039
+ const focusedCell = table.focusedCell;
28040
+ const selectedRange = table.selectedRange;
28041
+ if (selectedRange) {
28042
+ const count = (selectedRange.end.row - selectedRange.start.row + 1) *
28043
+ (selectedRange.end.col - selectedRange.start.col + 1);
28044
+ this.cellSelectionDisplay.textContent = `${count} cells selected`;
28045
+ }
28046
+ else if (focusedCell) {
28047
+ this.cellSelectionDisplay.textContent = `Cell [${focusedCell.row}, ${focusedCell.col}]`;
28048
+ // Update cell formatting controls from focused cell
28049
+ const cell = table.getCell(focusedCell.row, focusedCell.col);
28050
+ if (cell) {
28051
+ if (this.cellBgColorInput) {
28052
+ this.cellBgColorInput.value = cell.backgroundColor || '#ffffff';
28053
+ }
28054
+ // Update border controls
28055
+ const border = cell.border;
28056
+ if (this.borderTopCheck)
28057
+ this.borderTopCheck.checked = border.top.style !== 'none';
28058
+ if (this.borderRightCheck)
28059
+ this.borderRightCheck.checked = border.right.style !== 'none';
28060
+ if (this.borderBottomCheck)
28061
+ this.borderBottomCheck.checked = border.bottom.style !== 'none';
28062
+ if (this.borderLeftCheck)
28063
+ this.borderLeftCheck.checked = border.left.style !== 'none';
28064
+ // Use first active border for properties
28065
+ const activeBorder = border.top.style !== 'none' ? border.top :
28066
+ border.right.style !== 'none' ? border.right :
28067
+ border.bottom.style !== 'none' ? border.bottom :
28068
+ border.left.style !== 'none' ? border.left : border.top;
28069
+ if (this.borderWidthInput)
28070
+ this.borderWidthInput.value = String(activeBorder.width);
28071
+ if (this.borderColorInput)
28072
+ this.borderColorInput.value = activeBorder.color;
28073
+ if (this.borderStyleSelect)
28074
+ this.borderStyleSelect.value = activeBorder.style;
28075
+ }
28076
+ }
28077
+ else {
28078
+ this.cellSelectionDisplay.textContent = 'No cell selected';
28079
+ }
28080
+ }
28081
+ insertRow() {
28082
+ if (!this.editor || !this.currentTable)
28083
+ return;
28084
+ const focusedCell = this.currentTable.focusedCell;
28085
+ const rowIndex = focusedCell ? focusedCell.row + 1 : this.currentTable.rowCount;
28086
+ this.editor.tableInsertRow(this.currentTable, rowIndex);
28087
+ this.updateFromFocusedTable();
28088
+ }
28089
+ removeRow() {
28090
+ if (!this.editor || !this.currentTable)
28091
+ return;
28092
+ const focusedCell = this.currentTable.focusedCell;
28093
+ if (focusedCell && this.currentTable.rowCount > 1) {
28094
+ this.editor.tableRemoveRow(this.currentTable, focusedCell.row);
28095
+ this.updateFromFocusedTable();
28096
+ }
28097
+ }
28098
+ insertColumn() {
28099
+ if (!this.editor || !this.currentTable)
28100
+ return;
28101
+ const focusedCell = this.currentTable.focusedCell;
28102
+ const colIndex = focusedCell ? focusedCell.col + 1 : this.currentTable.columnCount;
28103
+ this.editor.tableInsertColumn(this.currentTable, colIndex);
28104
+ this.updateFromFocusedTable();
28105
+ }
28106
+ removeColumn() {
28107
+ if (!this.editor || !this.currentTable)
28108
+ return;
28109
+ const focusedCell = this.currentTable.focusedCell;
28110
+ if (focusedCell && this.currentTable.columnCount > 1) {
28111
+ this.editor.tableRemoveColumn(this.currentTable, focusedCell.col);
28112
+ this.updateFromFocusedTable();
28113
+ }
28114
+ }
28115
+ applyHeaders() {
28116
+ if (!this.currentTable)
28117
+ return;
28118
+ if (this.headerRowInput) {
28119
+ const count = parseInt(this.headerRowInput.value, 10);
28120
+ this.currentTable.setHeaderRowCount(count);
28121
+ }
28122
+ if (this.headerColInput) {
28123
+ const count = parseInt(this.headerColInput.value, 10);
28124
+ this.currentTable.setHeaderColumnCount(count);
28125
+ }
28126
+ this.editor?.render();
28127
+ this.onApplyCallback?.(true);
28128
+ }
28129
+ applyDefaults() {
28130
+ if (!this.currentTable)
28131
+ return;
28132
+ if (this.defaultPaddingInput) {
28133
+ this.currentTable.defaultCellPadding = parseInt(this.defaultPaddingInput.value, 10);
28134
+ }
28135
+ if (this.defaultBorderColorInput) {
28136
+ this.currentTable.defaultBorderColor = this.defaultBorderColorInput.value;
28137
+ }
28138
+ this.editor?.render();
28139
+ this.onApplyCallback?.(true);
28140
+ }
28141
+ applyCellFormatting() {
28142
+ if (!this.currentTable)
28143
+ return;
28144
+ const focusedCell = this.currentTable.focusedCell;
28145
+ const selectedRange = this.currentTable.selectedRange;
28146
+ // Determine cells to update
28147
+ const cells = [];
28148
+ if (selectedRange) {
28149
+ for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
28150
+ for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
28151
+ cells.push({ row, col });
28152
+ }
28153
+ }
28154
+ }
28155
+ else if (focusedCell) {
28156
+ cells.push(focusedCell);
28157
+ }
28158
+ if (cells.length === 0)
28159
+ return;
28160
+ // Build border config
28161
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
28162
+ const color = this.borderColorInput?.value || '#cccccc';
28163
+ const style = (this.borderStyleSelect?.value || 'solid');
28164
+ const borderSide = { width, color, style };
28165
+ const noneBorder = { width: 0, color: '#000000', style: 'none' };
28166
+ const border = {
28167
+ top: this.borderTopCheck?.checked ? { ...borderSide } : { ...noneBorder },
28168
+ right: this.borderRightCheck?.checked ? { ...borderSide } : { ...noneBorder },
28169
+ bottom: this.borderBottomCheck?.checked ? { ...borderSide } : { ...noneBorder },
28170
+ left: this.borderLeftCheck?.checked ? { ...borderSide } : { ...noneBorder }
28171
+ };
28172
+ const bgColor = this.cellBgColorInput?.value;
28173
+ // Apply to each cell
28174
+ for (const { row, col } of cells) {
28175
+ const cell = this.currentTable.getCell(row, col);
28176
+ if (cell) {
28177
+ if (bgColor) {
28178
+ cell.backgroundColor = bgColor;
28179
+ }
28180
+ cell.border = border;
28181
+ }
28182
+ }
28183
+ this.editor?.render();
28184
+ this.onApplyCallback?.(true);
28185
+ }
28186
+ /**
28187
+ * Get the currently focused table.
28188
+ */
28189
+ getCurrentTable() {
28190
+ return this.currentTable;
28191
+ }
28192
+ /**
28193
+ * Check if a table is currently focused.
28194
+ */
28195
+ hasTable() {
28196
+ return this.currentTable !== null;
28197
+ }
28198
+ createRowLoop() {
28199
+ if (!this.editor || !this.currentTable) {
28200
+ this.onApplyCallback?.(false, new Error('No table focused'));
28201
+ return;
28202
+ }
28203
+ const fieldPath = this.loopFieldInput?.value.trim() || '';
28204
+ if (!fieldPath) {
28205
+ this.onApplyCallback?.(false, new Error('Array field path is required'));
28206
+ 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);
28212
+ }
28213
+ /**
28214
+ * Update the pane from current editor state.
28215
+ */
28216
+ update() {
28217
+ this.updateFromFocusedTable();
28218
+ }
28219
+ }
28220
+
28221
+ 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 };
25162
28222
  //# sourceMappingURL=pc-editor.esm.js.map