@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
package/dist/pc-editor.js CHANGED
@@ -839,12 +839,13 @@ class TextFormattingManager extends EventEmitter {
839
839
  }
840
840
  /**
841
841
  * Get formatting at a specific character position.
842
- * Returns the position-specific formatting or the default.
842
+ * Returns the position-specific formatting merged with defaults,
843
+ * ensuring all properties are consistently present.
843
844
  */
844
845
  getFormattingAt(position) {
845
846
  const override = this.formatting.get(position);
846
847
  if (override) {
847
- return { ...override };
848
+ return { ...this._defaultFormatting, ...override };
848
849
  }
849
850
  return { ...this._defaultFormatting };
850
851
  }
@@ -864,6 +865,20 @@ class TextFormattingManager extends EventEmitter {
864
865
  this.emit('formatting-changed', { start, end, formatting });
865
866
  }
866
867
  }
868
+ /**
869
+ * Set formatting at a specific position, replacing all existing formatting.
870
+ * Unlike applyFormatting which merges, this replaces completely.
871
+ * Used by undo operations to restore exact previous state.
872
+ * @param position Character position
873
+ * @param formatting Complete formatting to set
874
+ * @param silent If true, don't emit the formatting-changed event
875
+ */
876
+ setFormattingAt(position, formatting, silent = false) {
877
+ this.formatting.set(position, { ...formatting });
878
+ if (!silent) {
879
+ this.emit('formatting-changed', { start: position, end: position + 1, formatting });
880
+ }
881
+ }
867
882
  /**
868
883
  * Remove formatting from a range, reverting to default.
869
884
  */
@@ -917,6 +932,43 @@ class TextFormattingManager extends EventEmitter {
917
932
  getAllFormatting() {
918
933
  return new Map(this.formatting);
919
934
  }
935
+ /**
936
+ * Get formatting as compressed runs for serialization.
937
+ * Only outputs entries where formatting changes from the previous character.
938
+ * Skips leading default formatting to minimize output size.
939
+ * @param textLength Length of the text to serialize formatting for
940
+ */
941
+ getCompressedRuns(textLength) {
942
+ const runs = [];
943
+ const defaultFormat = this._defaultFormatting;
944
+ let lastFormat = null;
945
+ for (let i = 0; i < textLength; i++) {
946
+ const currentFormat = this.getFormattingAt(i);
947
+ const formatChanged = lastFormat === null ||
948
+ currentFormat.fontFamily !== lastFormat.fontFamily ||
949
+ currentFormat.fontSize !== lastFormat.fontSize ||
950
+ currentFormat.fontWeight !== lastFormat.fontWeight ||
951
+ currentFormat.fontStyle !== lastFormat.fontStyle ||
952
+ currentFormat.color !== lastFormat.color ||
953
+ currentFormat.backgroundColor !== lastFormat.backgroundColor;
954
+ if (formatChanged) {
955
+ const isDefault = currentFormat.fontFamily === defaultFormat.fontFamily &&
956
+ currentFormat.fontSize === defaultFormat.fontSize &&
957
+ currentFormat.fontWeight === defaultFormat.fontWeight &&
958
+ currentFormat.fontStyle === defaultFormat.fontStyle &&
959
+ currentFormat.color === defaultFormat.color &&
960
+ currentFormat.backgroundColor === defaultFormat.backgroundColor;
961
+ if (!isDefault || runs.length > 0) {
962
+ runs.push({
963
+ index: i,
964
+ formatting: { ...currentFormat }
965
+ });
966
+ }
967
+ lastFormat = currentFormat;
968
+ }
969
+ }
970
+ return runs;
971
+ }
920
972
  /**
921
973
  * Restore formatting from a map (for deserialization).
922
974
  */
@@ -3820,6 +3872,35 @@ class ImageObject extends BaseEmbeddedObject {
3820
3872
  get hasError() {
3821
3873
  return this._error;
3822
3874
  }
3875
+ /**
3876
+ * Get the loaded HTMLImageElement, if available.
3877
+ * Used by PDFGenerator to convert unsupported formats to PNG.
3878
+ */
3879
+ get imageElement() {
3880
+ return this._loaded ? this._image : null;
3881
+ }
3882
+ /**
3883
+ * Convert the image to a PNG data URL via canvas.
3884
+ * Used when the original format (e.g., SVG, WebP, GIF) is not supported by pdf-lib.
3885
+ * Returns null if the image is not loaded or conversion fails.
3886
+ */
3887
+ toPngDataUrl() {
3888
+ if (!this._loaded || !this._image)
3889
+ return null;
3890
+ try {
3891
+ const canvas = document.createElement('canvas');
3892
+ canvas.width = this._image.naturalWidth || this._size.width;
3893
+ canvas.height = this._image.naturalHeight || this._size.height;
3894
+ const ctx = canvas.getContext('2d');
3895
+ if (!ctx)
3896
+ return null;
3897
+ ctx.drawImage(this._image, 0, 0, canvas.width, canvas.height);
3898
+ return canvas.toDataURL('image/png');
3899
+ }
3900
+ catch {
3901
+ return null;
3902
+ }
3903
+ }
3823
3904
  loadImage() {
3824
3905
  if (!this._src) {
3825
3906
  this._error = true;
@@ -4715,12 +4796,10 @@ class TextBoxObject extends BaseEmbeddedObject {
4715
4796
  this.editing = false;
4716
4797
  }
4717
4798
  toData() {
4718
- // Serialize formatting map to array of [index, style] pairs
4719
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
4720
- const formattingEntries = [];
4721
- formattingMap.forEach((value, key) => {
4722
- formattingEntries.push([key, { ...value }]);
4723
- });
4799
+ // Serialize formatting as compressed runs (only at change boundaries)
4800
+ const text = this._flowingContent.getText();
4801
+ const compressedRuns = this._flowingContent.getFormattingManager().getCompressedRuns(text.length);
4802
+ const formattingEntries = compressedRuns.map(run => [run.index, { ...run.formatting }]);
4724
4803
  // Get substitution fields as array
4725
4804
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
4726
4805
  return {
@@ -4779,12 +4858,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4779
4858
  this._border = { ...boxData.border };
4780
4859
  if (boxData.padding !== undefined)
4781
4860
  this._padding = boxData.padding;
4782
- // Restore formatting runs
4783
- if (boxData.formattingRuns) {
4861
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
4862
+ if (boxData.formattingRuns && boxData.formattingRuns.length > 0) {
4784
4863
  const formattingManager = this._flowingContent.getFormattingManager();
4785
4864
  formattingManager.clear();
4786
- for (const [index, style] of boxData.formattingRuns) {
4787
- formattingManager.applyFormatting(index, index + 1, style);
4865
+ const textLength = this._flowingContent.getText().length;
4866
+ for (let i = 0; i < boxData.formattingRuns.length; i++) {
4867
+ const [startIndex, style] = boxData.formattingRuns[i];
4868
+ const nextIndex = i + 1 < boxData.formattingRuns.length
4869
+ ? boxData.formattingRuns[i + 1][0]
4870
+ : textLength;
4871
+ if (startIndex < nextIndex) {
4872
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
4873
+ }
4788
4874
  }
4789
4875
  }
4790
4876
  // Restore substitution fields
@@ -4934,15 +5020,19 @@ class TextBoxObject extends BaseEmbeddedObject {
4934
5020
  }
4935
5021
  /**
4936
5022
  * Check if a point is within this text box region.
5023
+ * Uses the full object bounds (including padding/border) so that
5024
+ * double-click to enter edit mode works on the entire text box area,
5025
+ * not just the inner text area.
4937
5026
  * @param point Point in canvas coordinates
4938
- * @param pageIndex The page index (ignored for text boxes)
5027
+ * @param _pageIndex The page index (ignored for text boxes)
4939
5028
  */
4940
- containsPointInRegion(point, pageIndex) {
4941
- const bounds = this.getRegionBounds(pageIndex);
4942
- if (!bounds)
5029
+ containsPointInRegion(point, _pageIndex) {
5030
+ if (!this._renderedPosition)
4943
5031
  return false;
4944
- return point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
4945
- point.y >= bounds.y && point.y <= bounds.y + bounds.height;
5032
+ return point.x >= this._renderedPosition.x &&
5033
+ point.x <= this._renderedPosition.x + this._size.width &&
5034
+ point.y >= this._renderedPosition.y &&
5035
+ point.y <= this._renderedPosition.y + this._size.height;
4946
5036
  }
4947
5037
  /**
4948
5038
  * Trigger a reflow of text in this text box.
@@ -5027,6 +5117,41 @@ function getVerticalPadding(padding) {
5027
5117
  return padding.top + padding.bottom;
5028
5118
  }
5029
5119
 
5120
+ /**
5121
+ * Logger - Centralized logging for PC Editor.
5122
+ *
5123
+ * When enabled, logs informational messages to the console.
5124
+ * When disabled, only errors are logged.
5125
+ * Controlled via EditorOptions.enableLogging or Logger.setEnabled().
5126
+ */
5127
+ let _enabled = false;
5128
+ const Logger = {
5129
+ /** Enable or disable logging. When disabled, only errors are logged. */
5130
+ setEnabled(enabled) {
5131
+ _enabled = enabled;
5132
+ },
5133
+ /** Check if logging is enabled. */
5134
+ isEnabled() {
5135
+ return _enabled;
5136
+ },
5137
+ /** Log an informational message. Only outputs when logging is enabled. */
5138
+ log(...args) {
5139
+ if (_enabled) {
5140
+ console.log(...args);
5141
+ }
5142
+ },
5143
+ /** Log a warning. Only outputs when logging is enabled. */
5144
+ warn(...args) {
5145
+ if (_enabled) {
5146
+ console.warn(...args);
5147
+ }
5148
+ },
5149
+ /** Log an error. Always outputs regardless of logging state. */
5150
+ error(...args) {
5151
+ console.error(...args);
5152
+ }
5153
+ };
5154
+
5030
5155
  /**
5031
5156
  * TableCell - A cell within a table that contains editable text.
5032
5157
  * Implements EditableTextRegion for unified text interaction.
@@ -5077,7 +5202,7 @@ class TableCell extends EventEmitter {
5077
5202
  });
5078
5203
  // Prevent embedded objects in table cells (only substitution fields allowed)
5079
5204
  this._flowingContent.insertEmbeddedObject = () => {
5080
- console.warn('Embedded objects are not allowed in table cells. Use insertSubstitutionField instead.');
5205
+ Logger.warn('[pc-editor:TableCell] Embedded objects are not allowed in table cells. Use insertSubstitutionField instead.');
5081
5206
  };
5082
5207
  // Set initial content
5083
5208
  if (config.content) {
@@ -5394,7 +5519,7 @@ class TableCell extends EventEmitter {
5394
5519
  this._reflowDirty = false;
5395
5520
  this._lastReflowWidth = width;
5396
5521
  this._cachedContentHeight = null; // Clear cached height since lines changed
5397
- console.log('[TableCell.reflow] cellId:', this._id, 'text:', JSON.stringify(this._flowingContent.getText()), 'lines:', this._flowedLines.length);
5522
+ Logger.log('[pc-editor:TableCell.reflow] cellId:', this._id, 'text:', JSON.stringify(this._flowingContent.getText()), 'lines:', this._flowedLines.length);
5398
5523
  }
5399
5524
  /**
5400
5525
  * Mark this cell as needing reflow.
@@ -5432,7 +5557,7 @@ class TableCell extends EventEmitter {
5432
5557
  return this._editing && this._flowingContent.hasFocus();
5433
5558
  }
5434
5559
  handleKeyDown(e) {
5435
- console.log('[TableCell.handleKeyDown] Key:', e.key, '_editing:', this._editing, 'flowingContent.hasFocus:', this._flowingContent.hasFocus());
5560
+ Logger.log('[pc-editor:TableCell.handleKeyDown] Key:', e.key, '_editing:', this._editing, 'flowingContent.hasFocus:', this._flowingContent.hasFocus());
5436
5561
  if (!this._editing)
5437
5562
  return false;
5438
5563
  // Let parent table handle Tab navigation
@@ -5440,9 +5565,9 @@ class TableCell extends EventEmitter {
5440
5565
  return false; // Not handled - parent will handle
5441
5566
  }
5442
5567
  // Delegate to FlowingTextContent
5443
- console.log('[TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5568
+ Logger.log('[pc-editor:TableCell.handleKeyDown] Delegating to FlowingTextContent.handleKeyDown');
5444
5569
  const handled = this._flowingContent.handleKeyDown(e);
5445
- console.log('[TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5570
+ Logger.log('[pc-editor:TableCell.handleKeyDown] FlowingTextContent handled:', handled);
5446
5571
  return handled;
5447
5572
  }
5448
5573
  onCursorBlink(handler) {
@@ -5514,28 +5639,49 @@ class TableCell extends EventEmitter {
5514
5639
  // Serialization
5515
5640
  // ============================================
5516
5641
  toData() {
5517
- const formattingMap = this._flowingContent.getFormattingManager().getAllFormatting();
5518
- const formattingRuns = [];
5519
- formattingMap.forEach((style, index) => {
5520
- formattingRuns.push([index, { ...style }]);
5521
- });
5642
+ const text = this._flowingContent.getText();
5643
+ const compressedRuns = this._flowingContent.getFormattingManager().getCompressedRuns(text.length);
5644
+ const formattingRuns = compressedRuns.map(run => [run.index, run.formatting]);
5522
5645
  // Get substitution fields for serialization
5523
5646
  const fields = this._flowingContent.getSubstitutionFieldManager().getFieldsArray();
5524
- return {
5525
- id: this._id,
5526
- rowSpan: this._rowSpan,
5527
- colSpan: this._colSpan,
5528
- backgroundColor: this._backgroundColor,
5529
- border: this._border,
5530
- padding: this._padding,
5531
- verticalAlign: this._verticalAlign,
5532
- content: this._flowingContent.getText(),
5533
- fontFamily: this._fontFamily,
5534
- fontSize: this._fontSize,
5535
- color: this._color,
5536
- formattingRuns: formattingRuns.length > 0 ? formattingRuns : undefined,
5537
- substitutionFields: fields.length > 0 ? fields : undefined
5538
- };
5647
+ // Only include non-default values to minimize export size
5648
+ const defaults = DEFAULT_TABLE_STYLE;
5649
+ const defaultBorder = DEFAULT_CELL_BORDER_SIDE;
5650
+ const isDefaultBorderSide = (side) => side.width === defaultBorder.width && side.color === defaultBorder.color && side.style === defaultBorder.style;
5651
+ const isDefaultBorder = isDefaultBorderSide(this._border.top) &&
5652
+ isDefaultBorderSide(this._border.right) &&
5653
+ isDefaultBorderSide(this._border.bottom) &&
5654
+ isDefaultBorderSide(this._border.left);
5655
+ const isDefaultPadding = this._padding.top === defaults.cellPadding &&
5656
+ this._padding.right === defaults.cellPadding &&
5657
+ this._padding.bottom === defaults.cellPadding &&
5658
+ this._padding.left === defaults.cellPadding;
5659
+ const data = {};
5660
+ if (this._rowSpan !== 1)
5661
+ data.rowSpan = this._rowSpan;
5662
+ if (this._colSpan !== 1)
5663
+ data.colSpan = this._colSpan;
5664
+ if (this._backgroundColor !== defaults.backgroundColor)
5665
+ data.backgroundColor = this._backgroundColor;
5666
+ if (!isDefaultBorder)
5667
+ data.border = this._border;
5668
+ if (!isDefaultPadding)
5669
+ data.padding = this._padding;
5670
+ if (this._verticalAlign !== 'top')
5671
+ data.verticalAlign = this._verticalAlign;
5672
+ if (text)
5673
+ data.content = text;
5674
+ if (this._fontFamily !== defaults.fontFamily)
5675
+ data.fontFamily = this._fontFamily;
5676
+ if (this._fontSize !== defaults.fontSize)
5677
+ data.fontSize = this._fontSize;
5678
+ if (this._color !== defaults.color)
5679
+ data.color = this._color;
5680
+ if (formattingRuns.length > 0)
5681
+ data.formattingRuns = formattingRuns;
5682
+ if (fields.length > 0)
5683
+ data.substitutionFields = fields;
5684
+ return data;
5539
5685
  }
5540
5686
  static fromData(data) {
5541
5687
  const cell = new TableCell({
@@ -5551,14 +5697,19 @@ class TableCell extends EventEmitter {
5551
5697
  fontSize: data.fontSize,
5552
5698
  color: data.color
5553
5699
  });
5554
- // Restore formatting runs
5555
- if (data.formattingRuns) {
5700
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
5701
+ if (data.formattingRuns && data.formattingRuns.length > 0) {
5556
5702
  const formattingManager = cell._flowingContent.getFormattingManager();
5557
- const formattingMap = new Map();
5558
- for (const [index, style] of data.formattingRuns) {
5559
- formattingMap.set(index, style);
5703
+ const textLength = (data.content || '').length;
5704
+ for (let i = 0; i < data.formattingRuns.length; i++) {
5705
+ const [startIndex, style] = data.formattingRuns[i];
5706
+ const nextIndex = i + 1 < data.formattingRuns.length
5707
+ ? data.formattingRuns[i + 1][0]
5708
+ : textLength;
5709
+ if (startIndex < nextIndex) {
5710
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
5711
+ }
5560
5712
  }
5561
- formattingManager.setAllFormatting(formattingMap);
5562
5713
  }
5563
5714
  // Restore substitution fields
5564
5715
  if (data.substitutionFields && Array.isArray(data.substitutionFields)) {
@@ -5792,13 +5943,17 @@ class TableRow extends EventEmitter {
5792
5943
  // Serialization
5793
5944
  // ============================================
5794
5945
  toData() {
5795
- return {
5796
- id: this._id,
5797
- height: this._height,
5798
- minHeight: this._minHeight,
5799
- isHeader: this._isHeader,
5946
+ const data = {
5800
5947
  cells: this._cells.map(cell => cell.toData())
5801
5948
  };
5949
+ // Only include non-default values
5950
+ if (this._height !== null)
5951
+ data.height = this._height;
5952
+ if (this._minHeight !== DEFAULT_TABLE_STYLE.minRowHeight)
5953
+ data.minHeight = this._minHeight;
5954
+ if (this._isHeader)
5955
+ data.isHeader = this._isHeader;
5956
+ return data;
5802
5957
  }
5803
5958
  static fromData(data) {
5804
5959
  const row = new TableRow({
@@ -6132,7 +6287,7 @@ class TableObject extends BaseEmbeddedObject {
6132
6287
  set position(value) {
6133
6288
  // Tables only support block positioning - ignore any attempt to set other modes
6134
6289
  if (value !== 'block') {
6135
- console.warn(`Tables only support 'block' positioning. Ignoring attempt to set '${value}'.`);
6290
+ Logger.warn(`[pc-editor:TableObject] Tables only support 'block' positioning. Ignoring attempt to set '${value}'.`);
6136
6291
  }
6137
6292
  // Always set to block
6138
6293
  super.position = 'block';
@@ -6671,24 +6826,24 @@ class TableObject extends BaseEmbeddedObject {
6671
6826
  createRowLoop(startRowIndex, endRowIndex, fieldPath) {
6672
6827
  // Validate range
6673
6828
  if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
6674
- console.warn('[TableObject.createRowLoop] Invalid row range');
6829
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Invalid row range');
6675
6830
  return null;
6676
6831
  }
6677
6832
  if (startRowIndex > endRowIndex) {
6678
- console.warn('[TableObject.createRowLoop] Start index must be <= end index');
6833
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Start index must be <= end index');
6679
6834
  return null;
6680
6835
  }
6681
6836
  // Check for overlap with existing loops
6682
6837
  for (const existingLoop of this._rowLoops.values()) {
6683
6838
  if (this.loopRangesOverlap(startRowIndex, endRowIndex, existingLoop.startRowIndex, existingLoop.endRowIndex)) {
6684
- console.warn('[TableObject.createRowLoop] Loop range overlaps with existing loop');
6839
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop range overlaps with existing loop');
6685
6840
  return null;
6686
6841
  }
6687
6842
  }
6688
6843
  // Check that loop rows are not header rows
6689
6844
  for (let i = startRowIndex; i <= endRowIndex; i++) {
6690
6845
  if (this._rows[i]?.isHeader) {
6691
- console.warn('[TableObject.createRowLoop] Loop rows cannot be header rows');
6846
+ Logger.warn('[pc-editor:TableObject.createRowLoop] Loop rows cannot be header rows');
6692
6847
  return null;
6693
6848
  }
6694
6849
  }
@@ -7494,7 +7649,7 @@ class TableObject extends BaseEmbeddedObject {
7494
7649
  return this._editing;
7495
7650
  }
7496
7651
  handleKeyDown(e) {
7497
- console.log('[TableObject.handleKeyDown] Key:', e.key, '_editing:', this._editing, '_focusedCell:', this._focusedCell);
7652
+ Logger.log('[pc-editor:TableObject.handleKeyDown] Key:', e.key, '_editing:', this._editing, '_focusedCell:', this._focusedCell);
7498
7653
  if (!this._editing)
7499
7654
  return false;
7500
7655
  // Handle Tab navigation
@@ -8282,14 +8437,20 @@ class EmbeddedObjectFactory {
8282
8437
  border: data.data.border,
8283
8438
  padding: data.data.padding
8284
8439
  });
8285
- // Restore formatting runs if present
8440
+ // Restore formatting runs (run-based: each entry applies from its index to the next)
8286
8441
  if (data.data.formattingRuns && Array.isArray(data.data.formattingRuns)) {
8287
- const formattingManager = textBox.flowingContent.getFormattingManager();
8288
- const formattingMap = new Map();
8289
- for (const [index, style] of data.data.formattingRuns) {
8290
- formattingMap.set(index, style);
8442
+ const runs = data.data.formattingRuns;
8443
+ if (runs.length > 0) {
8444
+ const formattingManager = textBox.flowingContent.getFormattingManager();
8445
+ const textLength = textBox.flowingContent.getText().length;
8446
+ for (let i = 0; i < runs.length; i++) {
8447
+ const [startIndex, style] = runs[i];
8448
+ const nextIndex = i + 1 < runs.length ? runs[i + 1][0] : textLength;
8449
+ if (startIndex < nextIndex) {
8450
+ formattingManager.applyFormatting(startIndex, nextIndex, style);
8451
+ }
8452
+ }
8291
8453
  }
8292
- formattingManager.setAllFormatting(formattingMap);
8293
8454
  }
8294
8455
  // Restore substitution fields if present
8295
8456
  if (data.data.substitutionFields && Array.isArray(data.data.substitutionFields)) {
@@ -9266,9 +9427,9 @@ class FlowingTextContent extends EventEmitter {
9266
9427
  * @returns true if the event was handled, false otherwise
9267
9428
  */
9268
9429
  handleKeyDown(e) {
9269
- console.log('[FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9430
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] Key:', e.key, '_hasFocus:', this._hasFocus);
9270
9431
  if (!this._hasFocus) {
9271
- console.log('[FlowingTextContent.handleKeyDown] No focus, returning false');
9432
+ Logger.log('[pc-editor:FlowingTextContent.handleKeyDown] No focus, returning false');
9272
9433
  return false;
9273
9434
  }
9274
9435
  switch (e.key) {
@@ -9783,46 +9944,19 @@ class FlowingTextContent extends EventEmitter {
9783
9944
  toData() {
9784
9945
  // Serialize text content
9785
9946
  const text = this.textState.getText();
9786
- // Serialize text formatting as runs - only output when format changes
9787
- // This optimizes document size by not storing redundant formatting entries
9788
- const formattingRuns = [];
9789
- const defaultFormat = this.formatting.defaultFormatting;
9790
- let lastFormat = null;
9791
- for (let i = 0; i < text.length; i++) {
9792
- const currentFormat = this.formatting.getFormattingAt(i);
9793
- // Check if formatting changed from previous character
9794
- const formatChanged = lastFormat === null ||
9795
- currentFormat.fontFamily !== lastFormat.fontFamily ||
9796
- currentFormat.fontSize !== lastFormat.fontSize ||
9797
- currentFormat.fontWeight !== lastFormat.fontWeight ||
9798
- currentFormat.fontStyle !== lastFormat.fontStyle ||
9799
- currentFormat.color !== lastFormat.color ||
9800
- currentFormat.backgroundColor !== lastFormat.backgroundColor;
9801
- if (formatChanged) {
9802
- // Only output if different from default (to further reduce size)
9803
- const isDefault = currentFormat.fontFamily === defaultFormat.fontFamily &&
9804
- currentFormat.fontSize === defaultFormat.fontSize &&
9805
- currentFormat.fontWeight === defaultFormat.fontWeight &&
9806
- currentFormat.fontStyle === defaultFormat.fontStyle &&
9807
- currentFormat.color === defaultFormat.color &&
9808
- currentFormat.backgroundColor === defaultFormat.backgroundColor;
9809
- // Always output first run if it's not default, or output when format changes
9810
- if (!isDefault || formattingRuns.length > 0) {
9811
- formattingRuns.push({
9812
- index: i,
9813
- formatting: {
9814
- fontFamily: currentFormat.fontFamily,
9815
- fontSize: currentFormat.fontSize,
9816
- fontWeight: currentFormat.fontWeight,
9817
- fontStyle: currentFormat.fontStyle,
9818
- color: currentFormat.color,
9819
- backgroundColor: currentFormat.backgroundColor
9820
- }
9821
- });
9822
- }
9823
- lastFormat = currentFormat;
9947
+ // Serialize text formatting as compressed runs (only at change boundaries)
9948
+ const compressedRuns = this.formatting.getCompressedRuns(text.length);
9949
+ const formattingRuns = compressedRuns.map(run => ({
9950
+ index: run.index,
9951
+ formatting: {
9952
+ fontFamily: run.formatting.fontFamily,
9953
+ fontSize: run.formatting.fontSize,
9954
+ fontWeight: run.formatting.fontWeight,
9955
+ fontStyle: run.formatting.fontStyle,
9956
+ color: run.formatting.color,
9957
+ backgroundColor: run.formatting.backgroundColor
9824
9958
  }
9825
- }
9959
+ }));
9826
9960
  // Serialize paragraph formatting
9827
9961
  const paragraphFormatting = this.paragraphFormatting.toJSON();
9828
9962
  // Serialize substitution fields
@@ -9918,7 +10052,7 @@ class FlowingTextContent extends EventEmitter {
9918
10052
  content.getEmbeddedObjectManager().insert(object, ref.textIndex);
9919
10053
  }
9920
10054
  else {
9921
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10055
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9922
10056
  }
9923
10057
  }
9924
10058
  }
@@ -9972,7 +10106,7 @@ class FlowingTextContent extends EventEmitter {
9972
10106
  this.embeddedObjects.insert(object, ref.textIndex);
9973
10107
  }
9974
10108
  else {
9975
- console.warn(`Failed to create embedded object of type: ${ref.object.objectType}`);
10109
+ Logger.warn(`[pc-editor:FlowingTextContent] Failed to create embedded object of type: ${ref.object.objectType}`);
9976
10110
  }
9977
10111
  }
9978
10112
  }
@@ -12511,7 +12645,7 @@ class FlowingTextRenderer extends EventEmitter {
12511
12645
  updateResizeHandleTargets(selectedObjects) {
12512
12646
  // Clear existing resize handle targets
12513
12647
  this._hitTestManager.clearCategory('resize-handles');
12514
- console.log('[updateResizeHandleTargets] selectedObjects:', selectedObjects.length);
12648
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets selectedObjects:', selectedObjects.length);
12515
12649
  // Register resize handles for each selected object
12516
12650
  for (const object of selectedObjects) {
12517
12651
  if (!object.resizable)
@@ -12524,7 +12658,7 @@ class FlowingTextRenderer extends EventEmitter {
12524
12658
  // For regular objects, use renderedPosition
12525
12659
  const pos = object.renderedPosition;
12526
12660
  const pageIndex = object.renderedPageIndex;
12527
- console.log('[updateResizeHandleTargets] object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12661
+ Logger.log('[pc-editor:FlowingTextRenderer] updateResizeHandleTargets object:', object.id, 'pageIndex:', pageIndex, 'pos:', pos);
12528
12662
  if (pos && pageIndex >= 0) {
12529
12663
  this.registerObjectResizeHandles(object, pageIndex, pos);
12530
12664
  }
@@ -15170,7 +15304,7 @@ class CanvasManager extends EventEmitter {
15170
15304
  this.emit('element-removed', { elementId: objectId });
15171
15305
  }
15172
15306
  selectElement(elementId) {
15173
- console.log('Selecting element:', elementId);
15307
+ Logger.log('[pc-editor:CanvasManager] Selecting element:', elementId);
15174
15308
  this.selectedElements.add(elementId);
15175
15309
  // Update embedded object's selected state
15176
15310
  const flowingContents = [
@@ -15182,12 +15316,12 @@ class CanvasManager extends EventEmitter {
15182
15316
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15183
15317
  for (const [, obj] of embeddedObjects.entries()) {
15184
15318
  if (obj.id === elementId) {
15185
- console.log('Found embedded object to select:', obj.id);
15319
+ Logger.log('[pc-editor:CanvasManager] Found embedded object to select:', obj.id);
15186
15320
  obj.selected = true;
15187
15321
  }
15188
15322
  }
15189
15323
  }
15190
- console.log('Selected elements after selection:', Array.from(this.selectedElements));
15324
+ Logger.log('[pc-editor:CanvasManager] Selected elements after selection:', Array.from(this.selectedElements));
15191
15325
  this.render();
15192
15326
  this.updateResizeHandleHitTargets();
15193
15327
  this.emit('selection-change', { selectedElements: Array.from(this.selectedElements) });
@@ -15237,10 +15371,10 @@ class CanvasManager extends EventEmitter {
15237
15371
  this.flowingTextRenderer.updateResizeHandleTargets(selectedObjects);
15238
15372
  }
15239
15373
  clearSelection() {
15240
- console.log('clearSelection called, current selected elements:', Array.from(this.selectedElements));
15374
+ Logger.log('[pc-editor:CanvasManager] clearSelection called, current selected elements:', Array.from(this.selectedElements));
15241
15375
  // Clear selected state on all embedded objects
15242
15376
  this.selectedElements.forEach(elementId => {
15243
- console.log('Clearing selection for element:', elementId);
15377
+ Logger.log('[pc-editor:CanvasManager] Clearing selection for element:', elementId);
15244
15378
  // Check embedded objects in all flowing content sources (body, header, footer)
15245
15379
  const flowingContents = [
15246
15380
  this.document.bodyFlowingContent,
@@ -15251,7 +15385,7 @@ class CanvasManager extends EventEmitter {
15251
15385
  const embeddedObjects = flowingContent.getEmbeddedObjects();
15252
15386
  for (const [, embeddedObj] of embeddedObjects.entries()) {
15253
15387
  if (embeddedObj.id === elementId) {
15254
- console.log('Clearing selection on embedded object:', elementId);
15388
+ Logger.log('[pc-editor:CanvasManager] Clearing selection on embedded object:', elementId);
15255
15389
  embeddedObj.selected = false;
15256
15390
  }
15257
15391
  }
@@ -15259,7 +15393,7 @@ class CanvasManager extends EventEmitter {
15259
15393
  });
15260
15394
  this.selectedElements.clear();
15261
15395
  this.selectedSectionId = null;
15262
- console.log('About to render after clearing selection...');
15396
+ Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
15263
15397
  this.render();
15264
15398
  this.updateResizeHandleHitTargets();
15265
15399
  this.emit('selection-change', { selectedElements: [] });
@@ -15508,7 +15642,7 @@ class CanvasManager extends EventEmitter {
15508
15642
  });
15509
15643
  // Handle substitution field clicks
15510
15644
  this.flowingTextRenderer.on('substitution-field-clicked', (data) => {
15511
- console.log('[substitution-field-clicked] Field:', data.field?.fieldName, 'Section:', data.section);
15645
+ Logger.log('[pc-editor:CanvasManager] substitution-field-clicked Field:', data.field?.fieldName, 'Section:', data.section);
15512
15646
  // Emit event for external handling (e.g., showing field properties panel)
15513
15647
  this.emit('substitution-field-clicked', data);
15514
15648
  });
@@ -15983,14 +16117,15 @@ class CanvasManager extends EventEmitter {
15983
16117
  this.editingTextBox = textBox;
15984
16118
  this._editingTextBoxPageId = pageId || null;
15985
16119
  if (textBox) {
15986
- // Use the unified focus system to handle focus/blur and cursor blink
15987
- // This blurs the previous control, hiding its cursor
15988
- this.setFocus(textBox);
15989
16120
  // Clear selection in main flowing content
15990
16121
  this.document.bodyFlowingContent.clearSelection();
15991
- // Select the text box
16122
+ // Select the text box visually (this calls setFocus(null) internally,
16123
+ // so we must set focus to the text box AFTER this call)
15992
16124
  this.clearSelection();
15993
16125
  this.selectInlineElement({ type: 'embedded-object', object: textBox, textIndex: textBox.textIndex });
16126
+ // Now set focus to the text box for editing — must be AFTER selectInlineElement
16127
+ // because selectBaseEmbeddedObject calls setFocus(null) which would undo it
16128
+ this.setFocus(textBox);
15994
16129
  this.emit('textbox-editing-started', { textBox });
15995
16130
  }
15996
16131
  else {
@@ -16729,7 +16864,15 @@ class PDFGenerator {
16729
16864
  // Check if it's a data URL we can embed
16730
16865
  if (src.startsWith('data:')) {
16731
16866
  try {
16732
- const embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16867
+ let embeddedImage = await this.embedImageFromDataUrl(pdfDoc, src);
16868
+ // If the format isn't directly supported (e.g., SVG, WebP, GIF),
16869
+ // convert to PNG via canvas and try again
16870
+ if (!embeddedImage) {
16871
+ const pngDataUrl = image.toPngDataUrl();
16872
+ if (pngDataUrl) {
16873
+ embeddedImage = await this.embedImageFromDataUrl(pdfDoc, pngDataUrl);
16874
+ }
16875
+ }
16733
16876
  if (embeddedImage) {
16734
16877
  // Calculate draw position/size based on fit mode
16735
16878
  const drawParams = this.calculateImageDrawParams(embeddedImage.width, embeddedImage.height, image.width, image.height, image.fit);
@@ -16752,7 +16895,7 @@ class PDFGenerator {
16752
16895
  }
16753
16896
  }
16754
16897
  catch (e) {
16755
- console.warn('Failed to embed image:', e);
16898
+ Logger.warn('[pc-editor:PDFGenerator] Failed to embed image:', e);
16756
16899
  }
16757
16900
  }
16758
16901
  // Fallback: draw placeholder rectangle for images we can't embed
@@ -18780,7 +18923,7 @@ class MutationUndo {
18780
18923
  this.undoTableStructure(mutation);
18781
18924
  break;
18782
18925
  default:
18783
- console.warn('Unknown mutation type for undo:', mutation.type);
18926
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for undo:', mutation.type);
18784
18927
  }
18785
18928
  }
18786
18929
  /**
@@ -18830,7 +18973,7 @@ class MutationUndo {
18830
18973
  this.redoTableStructure(mutation);
18831
18974
  break;
18832
18975
  default:
18833
- console.warn('Unknown mutation type for redo:', mutation.type);
18976
+ Logger.warn('[pc-editor:MutationUndo] Unknown mutation type for redo:', mutation.type);
18834
18977
  }
18835
18978
  }
18836
18979
  // --- Text Mutations ---
@@ -18860,11 +19003,11 @@ class MutationUndo {
18860
19003
  const data = mutation.data;
18861
19004
  // Restore deleted text
18862
19005
  content.insertTextAt(data.position, data.deletedText);
18863
- // Restore formatting
19006
+ // Restore formatting using setFormattingAt to replace completely
18864
19007
  if (data.deletedFormatting) {
18865
19008
  const fm = content.getFormattingManager();
18866
19009
  data.deletedFormatting.forEach((style, offset) => {
18867
- fm.applyFormatting(data.position + offset, data.position + offset + 1, style);
19010
+ fm.setFormattingAt(data.position + offset, style, true);
18868
19011
  });
18869
19012
  }
18870
19013
  // Restore substitution fields
@@ -18890,9 +19033,10 @@ class MutationUndo {
18890
19033
  return;
18891
19034
  const data = mutation.data;
18892
19035
  const fm = content.getFormattingManager();
18893
- // Restore previous formatting
19036
+ // Restore previous formatting using setFormattingAt to replace completely
19037
+ // (not merge, which would leave properties like backgroundColor intact)
18894
19038
  data.previousFormatting.forEach((style, offset) => {
18895
- fm.applyFormatting(data.start + offset, data.start + offset + 1, style);
19039
+ fm.setFormattingAt(data.start + offset, style, true);
18896
19040
  });
18897
19041
  }
18898
19042
  redoFormat(mutation) {
@@ -20161,13 +20305,13 @@ class PDFParser {
20161
20305
  }
20162
20306
  catch {
20163
20307
  // Skip images that fail to extract
20164
- console.warn(`Failed to extract image: ${imageName}`);
20308
+ Logger.warn(`[pc-editor:PDFParser] Failed to extract image: ${imageName}`);
20165
20309
  }
20166
20310
  }
20167
20311
  }
20168
20312
  }
20169
20313
  catch (error) {
20170
- console.warn('Image extraction failed:', error);
20314
+ Logger.warn('[pc-editor:PDFParser] Image extraction failed:', error);
20171
20315
  }
20172
20316
  return images;
20173
20317
  }
@@ -21194,6 +21338,8 @@ class PCEditor extends EventEmitter {
21194
21338
  }
21195
21339
  this.container = container;
21196
21340
  this.options = this.mergeOptions(options);
21341
+ // Initialize logging
21342
+ Logger.setEnabled(this.options.enableLogging ?? false);
21197
21343
  this.document = new Document();
21198
21344
  // Apply constructor options to document settings
21199
21345
  this.document.updateSettings({
@@ -21218,7 +21364,8 @@ class PCEditor extends EventEmitter {
21218
21364
  showControlCharacters: options?.showControlCharacters ?? false,
21219
21365
  defaultFont: options?.defaultFont || 'Arial',
21220
21366
  defaultFontSize: options?.defaultFontSize || 12,
21221
- theme: options?.theme || 'light'
21367
+ theme: options?.theme || 'light',
21368
+ enableLogging: options?.enableLogging ?? false
21222
21369
  };
21223
21370
  }
21224
21371
  initialize() {
@@ -21625,6 +21772,7 @@ class PCEditor extends EventEmitter {
21625
21772
  * This changes which section receives keyboard input and cursor positioning.
21626
21773
  */
21627
21774
  setActiveSection(section) {
21775
+ Logger.log('[pc-editor] setActiveSection', section);
21628
21776
  if (this._activeEditingSection !== section) {
21629
21777
  this._activeEditingSection = section;
21630
21778
  // Delegate to canvas manager which handles the section change and emits events
@@ -21715,6 +21863,7 @@ class PCEditor extends EventEmitter {
21715
21863
  }
21716
21864
  }
21717
21865
  loadDocument(documentData) {
21866
+ Logger.log('[pc-editor] loadDocument');
21718
21867
  if (!this._isReady) {
21719
21868
  throw new Error('Editor is not ready');
21720
21869
  }
@@ -21741,6 +21890,7 @@ class PCEditor extends EventEmitter {
21741
21890
  return this.document.toData();
21742
21891
  }
21743
21892
  bindData(data) {
21893
+ Logger.log('[pc-editor] bindData');
21744
21894
  if (!this._isReady) {
21745
21895
  throw new Error('Editor is not ready');
21746
21896
  }
@@ -21749,6 +21899,7 @@ class PCEditor extends EventEmitter {
21749
21899
  this.emit('data-bound', { data });
21750
21900
  }
21751
21901
  async exportPDF(options) {
21902
+ Logger.log('[pc-editor] exportPDF');
21752
21903
  if (!this._isReady) {
21753
21904
  throw new Error('Editor is not ready');
21754
21905
  }
@@ -21790,6 +21941,7 @@ class PCEditor extends EventEmitter {
21790
21941
  * @returns JSON string representation of the document
21791
21942
  */
21792
21943
  saveDocument() {
21944
+ Logger.log('[pc-editor] saveDocument');
21793
21945
  if (!this._isReady) {
21794
21946
  throw new Error('Editor is not ready');
21795
21947
  }
@@ -21801,6 +21953,7 @@ class PCEditor extends EventEmitter {
21801
21953
  * @param filename Optional filename (defaults to 'document.pceditor.json')
21802
21954
  */
21803
21955
  saveDocumentToFile(filename = 'document.pceditor.json') {
21956
+ Logger.log('[pc-editor] saveDocumentToFile', filename);
21804
21957
  const jsonString = this.saveDocument();
21805
21958
  const blob = new Blob([jsonString], { type: 'application/json' });
21806
21959
  const url = URL.createObjectURL(blob);
@@ -21818,6 +21971,7 @@ class PCEditor extends EventEmitter {
21818
21971
  * @param jsonString JSON string representation of the document
21819
21972
  */
21820
21973
  loadDocumentFromJSON(jsonString) {
21974
+ Logger.log('[pc-editor] loadDocumentFromJSON');
21821
21975
  if (!this._isReady) {
21822
21976
  throw new Error('Editor is not ready');
21823
21977
  }
@@ -21838,6 +21992,7 @@ class PCEditor extends EventEmitter {
21838
21992
  * @returns Promise that resolves when loading is complete
21839
21993
  */
21840
21994
  async loadDocumentFromFile(file) {
21995
+ Logger.log('[pc-editor] loadDocumentFromFile', file.name);
21841
21996
  if (!this._isReady) {
21842
21997
  throw new Error('Editor is not ready');
21843
21998
  }
@@ -21926,22 +22081,25 @@ class PCEditor extends EventEmitter {
21926
22081
  // Version compatibility check
21927
22082
  const [major] = doc.version.split('.').map(Number);
21928
22083
  if (major > 1) {
21929
- console.warn(`Document version ${doc.version} may not be fully compatible with this editor`);
22084
+ Logger.warn(`[pc-editor] Document version ${doc.version} may not be fully compatible with this editor`);
21930
22085
  }
21931
22086
  }
21932
22087
  selectElement(elementId) {
22088
+ Logger.log('[pc-editor] selectElement', elementId);
21933
22089
  if (!this._isReady) {
21934
22090
  throw new Error('Editor is not ready');
21935
22091
  }
21936
22092
  this.canvasManager.selectElement(elementId);
21937
22093
  }
21938
22094
  clearSelection() {
22095
+ Logger.log('[pc-editor] clearSelection');
21939
22096
  if (!this._isReady) {
21940
22097
  throw new Error('Editor is not ready');
21941
22098
  }
21942
22099
  this.canvasManager.clearSelection();
21943
22100
  }
21944
22101
  removeEmbeddedObject(objectId) {
22102
+ Logger.log('[pc-editor] removeEmbeddedObject', objectId);
21945
22103
  if (!this._isReady) {
21946
22104
  throw new Error('Editor is not ready');
21947
22105
  }
@@ -21951,6 +22109,7 @@ class PCEditor extends EventEmitter {
21951
22109
  * Undo the last operation.
21952
22110
  */
21953
22111
  undo() {
22112
+ Logger.log('[pc-editor] undo');
21954
22113
  if (!this._isReady)
21955
22114
  return;
21956
22115
  const success = this.transactionManager.undo();
@@ -21963,6 +22122,7 @@ class PCEditor extends EventEmitter {
21963
22122
  * Redo the last undone operation.
21964
22123
  */
21965
22124
  redo() {
22125
+ Logger.log('[pc-editor] redo');
21966
22126
  if (!this._isReady)
21967
22127
  return;
21968
22128
  const success = this.transactionManager.redo();
@@ -21996,16 +22156,19 @@ class PCEditor extends EventEmitter {
21996
22156
  this.transactionManager.setMaxHistory(count);
21997
22157
  }
21998
22158
  zoomIn() {
22159
+ Logger.log('[pc-editor] zoomIn');
21999
22160
  if (!this._isReady)
22000
22161
  return;
22001
22162
  this.canvasManager.zoomIn();
22002
22163
  }
22003
22164
  zoomOut() {
22165
+ Logger.log('[pc-editor] zoomOut');
22004
22166
  if (!this._isReady)
22005
22167
  return;
22006
22168
  this.canvasManager.zoomOut();
22007
22169
  }
22008
22170
  setZoom(level) {
22171
+ Logger.log('[pc-editor] setZoom', level);
22009
22172
  if (!this._isReady)
22010
22173
  return;
22011
22174
  this.canvasManager.setZoom(level);
@@ -22042,6 +22205,7 @@ class PCEditor extends EventEmitter {
22042
22205
  return this.canvasManager.getContentOffset();
22043
22206
  }
22044
22207
  fitToWidth() {
22208
+ Logger.log('[pc-editor] fitToWidth');
22045
22209
  if (!this._isReady)
22046
22210
  return;
22047
22211
  this.canvasManager.fitToWidth();
@@ -22050,23 +22214,27 @@ class PCEditor extends EventEmitter {
22050
22214
  * Force a re-render of the canvas.
22051
22215
  */
22052
22216
  render() {
22217
+ Logger.log('[pc-editor] render');
22053
22218
  if (!this._isReady)
22054
22219
  return;
22055
22220
  this.canvasManager.render();
22056
22221
  }
22057
22222
  fitToPage() {
22223
+ Logger.log('[pc-editor] fitToPage');
22058
22224
  if (!this._isReady)
22059
22225
  return;
22060
22226
  this.canvasManager.fitToPage();
22061
22227
  }
22062
22228
  // Layout control methods
22063
22229
  setAutoFlow(enabled) {
22230
+ Logger.log('[pc-editor] setAutoFlow', enabled);
22064
22231
  if (!this._isReady) {
22065
22232
  throw new Error('Editor is not ready');
22066
22233
  }
22067
22234
  this.layoutEngine.setAutoFlow(enabled);
22068
22235
  }
22069
22236
  reflowDocument() {
22237
+ Logger.log('[pc-editor] reflowDocument');
22070
22238
  if (!this._isReady) {
22071
22239
  throw new Error('Editor is not ready');
22072
22240
  }
@@ -22108,6 +22276,7 @@ class PCEditor extends EventEmitter {
22108
22276
  };
22109
22277
  }
22110
22278
  addPage() {
22279
+ Logger.log('[pc-editor] addPage');
22111
22280
  if (!this._isReady) {
22112
22281
  throw new Error('Editor is not ready');
22113
22282
  }
@@ -22119,6 +22288,7 @@ class PCEditor extends EventEmitter {
22119
22288
  this.canvasManager.setDocument(this.document);
22120
22289
  }
22121
22290
  removePage(pageId) {
22291
+ Logger.log('[pc-editor] removePage', pageId);
22122
22292
  if (!this._isReady) {
22123
22293
  throw new Error('Editor is not ready');
22124
22294
  }
@@ -22193,7 +22363,7 @@ class PCEditor extends EventEmitter {
22193
22363
  }
22194
22364
  // Use the unified focus system to get the currently focused control
22195
22365
  const focusedControl = this.canvasManager.getFocusedControl();
22196
- console.log('[PCEditor.handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22366
+ Logger.log('[pc-editor:handleKeyDown] Key:', e.key, 'focusedControl:', focusedControl?.constructor?.name);
22197
22367
  if (!focusedControl)
22198
22368
  return;
22199
22369
  // Vertical navigation needs layout context - handle specially
@@ -22217,9 +22387,9 @@ class PCEditor extends EventEmitter {
22217
22387
  }
22218
22388
  }
22219
22389
  // Delegate to the focused control's handleKeyDown
22220
- console.log('[PCEditor.handleKeyDown] Calling focusedControl.handleKeyDown');
22390
+ Logger.log('[pc-editor:handleKeyDown] Calling focusedControl.handleKeyDown');
22221
22391
  const handled = focusedControl.handleKeyDown(e);
22222
- console.log('[PCEditor.handleKeyDown] handled:', handled);
22392
+ Logger.log('[pc-editor:handleKeyDown] handled:', handled);
22223
22393
  if (handled) {
22224
22394
  this.canvasManager.render();
22225
22395
  // Handle text box-specific post-processing
@@ -22583,6 +22753,7 @@ class PCEditor extends EventEmitter {
22583
22753
  * This is useful when UI controls have stolen focus.
22584
22754
  */
22585
22755
  applyFormattingWithFallback(start, end, formatting) {
22756
+ Logger.log('[pc-editor] applyFormattingWithFallback', start, end, formatting);
22586
22757
  // Try current context first
22587
22758
  let flowingContent = this.getEditingFlowingContent();
22588
22759
  // Fall back to saved context
@@ -22783,6 +22954,7 @@ class PCEditor extends EventEmitter {
22783
22954
  * Works for body text, text boxes, and table cells.
22784
22955
  */
22785
22956
  setUnifiedAlignment(alignment) {
22957
+ Logger.log('[pc-editor] setUnifiedAlignment', alignment);
22786
22958
  const flowingContent = this.getEditingFlowingContent();
22787
22959
  if (!flowingContent) {
22788
22960
  throw new Error('No text is being edited');
@@ -22799,6 +22971,7 @@ class PCEditor extends EventEmitter {
22799
22971
  this.canvasManager.render();
22800
22972
  }
22801
22973
  insertText(text) {
22974
+ Logger.log('[pc-editor] insertText', text);
22802
22975
  if (!this._isReady) {
22803
22976
  throw new Error('Editor is not ready');
22804
22977
  }
@@ -22815,6 +22988,7 @@ class PCEditor extends EventEmitter {
22815
22988
  return flowingContent ? flowingContent.getText() : '';
22816
22989
  }
22817
22990
  setFlowingText(text) {
22991
+ Logger.log('[pc-editor] setFlowingText');
22818
22992
  if (!this._isReady) {
22819
22993
  throw new Error('Editor is not ready');
22820
22994
  }
@@ -22828,6 +23002,7 @@ class PCEditor extends EventEmitter {
22828
23002
  * Works for body, header, footer, text boxes, and table cells.
22829
23003
  */
22830
23004
  setCursorPosition(position) {
23005
+ Logger.log('[pc-editor] setCursorPosition', position);
22831
23006
  if (!this._isReady) {
22832
23007
  throw new Error('Editor is not ready');
22833
23008
  }
@@ -22873,6 +23048,7 @@ class PCEditor extends EventEmitter {
22873
23048
  * Works for body, header, footer, text boxes, and table cells.
22874
23049
  */
22875
23050
  insertEmbeddedObject(object, position = 'inline') {
23051
+ Logger.log('[pc-editor] insertEmbeddedObject', object.id, position);
22876
23052
  if (!this._isReady) {
22877
23053
  throw new Error('Editor is not ready');
22878
23054
  }
@@ -22899,6 +23075,7 @@ class PCEditor extends EventEmitter {
22899
23075
  * Works for body, header, footer, text boxes, and table cells.
22900
23076
  */
22901
23077
  insertSubstitutionField(fieldName, config) {
23078
+ Logger.log('[pc-editor] insertSubstitutionField', fieldName);
22902
23079
  if (!this._isReady) {
22903
23080
  throw new Error('Editor is not ready');
22904
23081
  }
@@ -22924,6 +23101,7 @@ class PCEditor extends EventEmitter {
22924
23101
  * @param displayFormat Optional format string (e.g., "Page %d" where %d is replaced by page number)
22925
23102
  */
22926
23103
  insertPageNumberField(displayFormat) {
23104
+ Logger.log('[pc-editor] insertPageNumberField');
22927
23105
  if (!this._isReady) {
22928
23106
  throw new Error('Editor is not ready');
22929
23107
  }
@@ -22949,6 +23127,7 @@ class PCEditor extends EventEmitter {
22949
23127
  * @param displayFormat Optional format string (e.g., "of %d" where %d is replaced by page count)
22950
23128
  */
22951
23129
  insertPageCountField(displayFormat) {
23130
+ Logger.log('[pc-editor] insertPageCountField');
22952
23131
  if (!this._isReady) {
22953
23132
  throw new Error('Editor is not ready');
22954
23133
  }
@@ -22974,6 +23153,7 @@ class PCEditor extends EventEmitter {
22974
23153
  * or table cells are not recommended as these don't span pages.
22975
23154
  */
22976
23155
  insertPageBreak() {
23156
+ Logger.log('[pc-editor] insertPageBreak');
22977
23157
  if (!this._isReady) {
22978
23158
  throw new Error('Editor is not ready');
22979
23159
  }
@@ -23109,6 +23289,144 @@ class PCEditor extends EventEmitter {
23109
23289
  return null;
23110
23290
  }
23111
23291
  // ============================================
23292
+ // Text Box Update Operations
23293
+ // ============================================
23294
+ /**
23295
+ * Update properties of a text box.
23296
+ * @param textBoxId The ID of the text box to update
23297
+ * @param updates The properties to update
23298
+ */
23299
+ updateTextBox(textBoxId, updates) {
23300
+ if (!this._isReady)
23301
+ return false;
23302
+ // Find the text box in all flowing contents
23303
+ const textBox = this.findTextBoxById(textBoxId);
23304
+ if (!textBox) {
23305
+ Logger.warn(`[pc-editor:updateTextBox] Text box not found: ${textBoxId}`);
23306
+ return false;
23307
+ }
23308
+ // Apply updates
23309
+ if (updates.position !== undefined) {
23310
+ textBox.position = updates.position;
23311
+ }
23312
+ if (updates.relativeOffset !== undefined) {
23313
+ textBox.relativeOffset = updates.relativeOffset;
23314
+ }
23315
+ if (updates.backgroundColor !== undefined) {
23316
+ textBox.backgroundColor = updates.backgroundColor;
23317
+ }
23318
+ if (updates.border !== undefined) {
23319
+ // Merge with existing border
23320
+ const existingBorder = textBox.border;
23321
+ textBox.border = {
23322
+ top: updates.border.top || existingBorder.top,
23323
+ right: updates.border.right || existingBorder.right,
23324
+ bottom: updates.border.bottom || existingBorder.bottom,
23325
+ left: updates.border.left || existingBorder.left
23326
+ };
23327
+ }
23328
+ if (updates.padding !== undefined) {
23329
+ textBox.padding = updates.padding;
23330
+ }
23331
+ this.render();
23332
+ this.emit('textbox-updated', { textBoxId, updates });
23333
+ return true;
23334
+ }
23335
+ /**
23336
+ * Find a text box by ID across all flowing contents.
23337
+ */
23338
+ findTextBoxById(textBoxId) {
23339
+ const flowingContents = [
23340
+ this.document.bodyFlowingContent,
23341
+ this.document.headerFlowingContent,
23342
+ this.document.footerFlowingContent
23343
+ ].filter(Boolean);
23344
+ for (const flowingContent of flowingContents) {
23345
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23346
+ for (const [, obj] of embeddedObjects.entries()) {
23347
+ if (obj.id === textBoxId && obj instanceof TextBoxObject) {
23348
+ return obj;
23349
+ }
23350
+ }
23351
+ }
23352
+ return null;
23353
+ }
23354
+ // ============================================
23355
+ // Image Update Operations
23356
+ // ============================================
23357
+ /**
23358
+ * Update properties of an image.
23359
+ * @param imageId The ID of the image to update
23360
+ * @param updates The properties to update
23361
+ */
23362
+ updateImage(imageId, updates) {
23363
+ if (!this._isReady)
23364
+ return false;
23365
+ // Find the image in all flowing contents
23366
+ const image = this.findImageById(imageId);
23367
+ if (!image) {
23368
+ Logger.warn(`[pc-editor:updateImage] Image not found: ${imageId}`);
23369
+ return false;
23370
+ }
23371
+ // Apply updates
23372
+ if (updates.position !== undefined) {
23373
+ image.position = updates.position;
23374
+ }
23375
+ if (updates.relativeOffset !== undefined) {
23376
+ image.relativeOffset = updates.relativeOffset;
23377
+ }
23378
+ if (updates.fit !== undefined) {
23379
+ image.fit = updates.fit;
23380
+ }
23381
+ if (updates.resizeMode !== undefined) {
23382
+ image.resizeMode = updates.resizeMode;
23383
+ }
23384
+ if (updates.alt !== undefined) {
23385
+ image.alt = updates.alt;
23386
+ }
23387
+ this.render();
23388
+ this.emit('image-updated', { imageId, updates });
23389
+ return true;
23390
+ }
23391
+ /**
23392
+ * Set the source of an image.
23393
+ * @param imageId The ID of the image
23394
+ * @param dataUrl The data URL of the new image source
23395
+ * @param options Optional sizing options
23396
+ */
23397
+ setImageSource(imageId, dataUrl, options) {
23398
+ if (!this._isReady)
23399
+ return false;
23400
+ const image = this.findImageById(imageId);
23401
+ if (!image) {
23402
+ Logger.warn(`[pc-editor:setImageSource] Image not found: ${imageId}`);
23403
+ return false;
23404
+ }
23405
+ image.setSource(dataUrl, options);
23406
+ this.render();
23407
+ this.emit('image-source-changed', { imageId });
23408
+ return true;
23409
+ }
23410
+ /**
23411
+ * Find an image by ID across all flowing contents.
23412
+ */
23413
+ findImageById(imageId) {
23414
+ const flowingContents = [
23415
+ this.document.bodyFlowingContent,
23416
+ this.document.headerFlowingContent,
23417
+ this.document.footerFlowingContent
23418
+ ].filter(Boolean);
23419
+ for (const flowingContent of flowingContents) {
23420
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23421
+ for (const [, obj] of embeddedObjects.entries()) {
23422
+ if (obj.id === imageId && obj instanceof ImageObject) {
23423
+ return obj;
23424
+ }
23425
+ }
23426
+ }
23427
+ return null;
23428
+ }
23429
+ // ============================================
23112
23430
  // Table Structure Operations (with undo support)
23113
23431
  // ============================================
23114
23432
  /**
@@ -23208,7 +23526,7 @@ class PCEditor extends EventEmitter {
23208
23526
  * @deprecated Use insertEmbeddedObject instead
23209
23527
  */
23210
23528
  insertInlineElement(_elementData, _position = 'inline') {
23211
- console.warn('insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23529
+ Logger.warn('[pc-editor] insertInlineElement is deprecated and no longer functional. Use insertEmbeddedObject instead.');
23212
23530
  }
23213
23531
  /**
23214
23532
  * Apply merge data to substitute all substitution fields with their values.
@@ -23933,18 +24251,37 @@ class PCEditor extends EventEmitter {
23933
24251
  return this.document.bodyFlowingContent.getParagraphBoundaries();
23934
24252
  }
23935
24253
  /**
23936
- * Create a repeating section in the body content.
23937
- * Note: Repeating sections are only supported in the body, not in header/footer.
23938
- * @param startIndex Text index at paragraph start (must be at a paragraph boundary)
23939
- * @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
24254
+ * Create a repeating section.
24255
+ *
24256
+ * If a table is currently being edited (focused), creates a table row loop
24257
+ * based on the focused cell's row. In this case, startIndex and endIndex
24258
+ * are ignored — the focused cell's row determines the loop range.
24259
+ *
24260
+ * Otherwise, creates a body text repeating section at the given paragraph boundaries.
24261
+ *
24262
+ * @param startIndex Text index at paragraph start (ignored for table row loops)
24263
+ * @param endIndex Text index at closing paragraph start (ignored for table row loops)
23940
24264
  * @param fieldPath The field path to the array to loop over (e.g., "items")
23941
- * @returns The created section, or null if boundaries are invalid
24265
+ * @returns The created section/loop, or null if creation failed
23942
24266
  */
23943
24267
  createRepeatingSection(startIndex, endIndex, fieldPath) {
23944
24268
  if (!this._isReady) {
23945
24269
  throw new Error('Editor is not ready');
23946
24270
  }
24271
+ // If a table is focused, create a row loop instead of a text repeating section
24272
+ const focusedTable = this.getFocusedTable();
24273
+ if (focusedTable && focusedTable.focusedCell) {
24274
+ Logger.log('[pc-editor] createRepeatingSection → table row loop', fieldPath);
24275
+ const row = focusedTable.focusedCell.row;
24276
+ const loop = focusedTable.createRowLoop(row, row, fieldPath);
24277
+ if (loop) {
24278
+ this.canvasManager.render();
24279
+ this.emit('table-row-loop-added', { table: focusedTable, loop });
24280
+ }
24281
+ return null; // Row loops are not RepeatingSections, return null
24282
+ }
23947
24283
  // Repeating sections only work in body (document-level)
24284
+ Logger.log('[pc-editor] createRepeatingSection', startIndex, endIndex, fieldPath);
23948
24285
  const section = this.document.bodyFlowingContent.createRepeatingSection(startIndex, endIndex, fieldPath);
23949
24286
  if (section) {
23950
24287
  this.canvasManager.render();
@@ -24322,7 +24659,7 @@ class PCEditor extends EventEmitter {
24322
24659
  createEmbeddedObjectFromData(data) {
24323
24660
  const object = EmbeddedObjectFactory.tryCreate(data);
24324
24661
  if (!object) {
24325
- console.warn('Unknown object type:', data.objectType);
24662
+ Logger.warn('[pc-editor] Unknown object type:', data.objectType);
24326
24663
  }
24327
24664
  return object;
24328
24665
  }
@@ -24351,6 +24688,13 @@ class PCEditor extends EventEmitter {
24351
24688
  return false;
24352
24689
  }
24353
24690
  }
24691
+ /**
24692
+ * Enable or disable verbose logging.
24693
+ * When disabled (default), only errors are logged to the console.
24694
+ */
24695
+ setLogging(enabled) {
24696
+ Logger.setEnabled(enabled);
24697
+ }
24354
24698
  destroy() {
24355
24699
  this.disableTextInput();
24356
24700
  if (this.canvasManager) {
@@ -25179,8 +25523,2725 @@ class VerticalRuler extends RulerControl {
25179
25523
  }
25180
25524
  }
25181
25525
 
25526
+ /**
25527
+ * BasePane - Abstract base class for editor property panes.
25528
+ *
25529
+ * Panes are property editors that work with PCEditor via the public API only.
25530
+ * They are content-only (no title bar) for flexible layout by consumers.
25531
+ */
25532
+ /**
25533
+ * Abstract base class for editor panes.
25534
+ */
25535
+ class BasePane extends BaseControl {
25536
+ constructor(id, options = {}) {
25537
+ super(id, options);
25538
+ this.sectionElement = null;
25539
+ this.className = options.className || '';
25540
+ }
25541
+ /**
25542
+ * Attach the pane to an editor.
25543
+ */
25544
+ attach(options) {
25545
+ // Store the section element if provided
25546
+ this.sectionElement = options.sectionElement || null;
25547
+ super.attach(options);
25548
+ }
25549
+ /**
25550
+ * Show the pane (and section element if provided).
25551
+ */
25552
+ show() {
25553
+ this._isVisible = true;
25554
+ if (this.sectionElement) {
25555
+ this.sectionElement.style.display = '';
25556
+ }
25557
+ if (this.element) {
25558
+ this.element.style.display = '';
25559
+ this.update();
25560
+ }
25561
+ this.emit('visibility-changed', { visible: true });
25562
+ }
25563
+ /**
25564
+ * Hide the pane (and section element if provided).
25565
+ */
25566
+ hide() {
25567
+ this._isVisible = false;
25568
+ if (this.sectionElement) {
25569
+ this.sectionElement.style.display = 'none';
25570
+ }
25571
+ if (this.element) {
25572
+ this.element.style.display = 'none';
25573
+ }
25574
+ this.emit('visibility-changed', { visible: false });
25575
+ }
25576
+ /**
25577
+ * Create a form group element with label.
25578
+ */
25579
+ createFormGroup(label, inputElement, options) {
25580
+ const group = document.createElement('div');
25581
+ group.className = 'pc-pane-form-group';
25582
+ if (options?.inline) {
25583
+ group.classList.add('pc-pane-form-group--inline');
25584
+ }
25585
+ const labelEl = document.createElement('label');
25586
+ labelEl.className = 'pc-pane-label';
25587
+ labelEl.textContent = label;
25588
+ group.appendChild(labelEl);
25589
+ group.appendChild(inputElement);
25590
+ if (options?.hint) {
25591
+ const hintEl = document.createElement('span');
25592
+ hintEl.className = 'pc-pane-hint';
25593
+ hintEl.textContent = options.hint;
25594
+ group.appendChild(hintEl);
25595
+ }
25596
+ return group;
25597
+ }
25598
+ /**
25599
+ * Create a text input element.
25600
+ */
25601
+ createTextInput(options) {
25602
+ const input = document.createElement('input');
25603
+ input.type = options?.type || 'text';
25604
+ input.className = 'pc-pane-input';
25605
+ if (options?.placeholder) {
25606
+ input.placeholder = options.placeholder;
25607
+ }
25608
+ if (options?.value !== undefined) {
25609
+ input.value = options.value;
25610
+ }
25611
+ return input;
25612
+ }
25613
+ /**
25614
+ * Create a number input element.
25615
+ */
25616
+ createNumberInput(options) {
25617
+ const input = document.createElement('input');
25618
+ input.type = 'number';
25619
+ input.className = 'pc-pane-input pc-pane-input--number';
25620
+ if (options?.min !== undefined)
25621
+ input.min = String(options.min);
25622
+ if (options?.max !== undefined)
25623
+ input.max = String(options.max);
25624
+ if (options?.step !== undefined)
25625
+ input.step = String(options.step);
25626
+ if (options?.value !== undefined)
25627
+ input.value = String(options.value);
25628
+ return input;
25629
+ }
25630
+ /**
25631
+ * Create a select element with options.
25632
+ */
25633
+ createSelect(optionsList, selectedValue) {
25634
+ const select = document.createElement('select');
25635
+ select.className = 'pc-pane-select';
25636
+ for (const opt of optionsList) {
25637
+ const option = document.createElement('option');
25638
+ option.value = opt.value;
25639
+ option.textContent = opt.label;
25640
+ if (opt.value === selectedValue) {
25641
+ option.selected = true;
25642
+ }
25643
+ select.appendChild(option);
25644
+ }
25645
+ return select;
25646
+ }
25647
+ /**
25648
+ * Create a color input element.
25649
+ */
25650
+ createColorInput(value) {
25651
+ const input = document.createElement('input');
25652
+ input.type = 'color';
25653
+ input.className = 'pc-pane-color';
25654
+ if (value) {
25655
+ input.value = value;
25656
+ }
25657
+ return input;
25658
+ }
25659
+ /**
25660
+ * Create a checkbox element.
25661
+ */
25662
+ createCheckbox(label, checked) {
25663
+ const wrapper = document.createElement('label');
25664
+ wrapper.className = 'pc-pane-checkbox';
25665
+ const input = document.createElement('input');
25666
+ input.type = 'checkbox';
25667
+ if (checked) {
25668
+ input.checked = true;
25669
+ }
25670
+ const span = document.createElement('span');
25671
+ span.textContent = label;
25672
+ wrapper.appendChild(input);
25673
+ wrapper.appendChild(span);
25674
+ return wrapper;
25675
+ }
25676
+ /**
25677
+ * Create a button element.
25678
+ */
25679
+ createButton(label, options) {
25680
+ const button = document.createElement('button');
25681
+ button.type = 'button';
25682
+ button.className = 'pc-pane-button';
25683
+ if (options?.variant) {
25684
+ button.classList.add(`pc-pane-button--${options.variant}`);
25685
+ }
25686
+ button.textContent = label;
25687
+ return button;
25688
+ }
25689
+ /**
25690
+ * Create a button group container.
25691
+ */
25692
+ createButtonGroup() {
25693
+ const group = document.createElement('div');
25694
+ group.className = 'pc-pane-button-group';
25695
+ return group;
25696
+ }
25697
+ /**
25698
+ * Create a section divider with optional label.
25699
+ */
25700
+ createSection(label) {
25701
+ const section = document.createElement('div');
25702
+ section.className = 'pc-pane-section';
25703
+ if (label) {
25704
+ const labelEl = document.createElement('div');
25705
+ labelEl.className = 'pc-pane-section-label';
25706
+ labelEl.textContent = label;
25707
+ section.appendChild(labelEl);
25708
+ }
25709
+ return section;
25710
+ }
25711
+ /**
25712
+ * Create a row container for inline elements.
25713
+ */
25714
+ createRow() {
25715
+ const row = document.createElement('div');
25716
+ row.className = 'pc-pane-row';
25717
+ return row;
25718
+ }
25719
+ /**
25720
+ * Create a hint/info text element.
25721
+ */
25722
+ createHint(text) {
25723
+ const hint = document.createElement('div');
25724
+ hint.className = 'pc-pane-hint';
25725
+ hint.textContent = text;
25726
+ return hint;
25727
+ }
25728
+ /**
25729
+ * Add immediate apply listener for text inputs (blur + Enter).
25730
+ */
25731
+ addImmediateApplyListener(element, handler) {
25732
+ const apply = () => {
25733
+ handler(element.value);
25734
+ };
25735
+ // Selects and color inputs: apply on change
25736
+ if (element instanceof HTMLSelectElement ||
25737
+ (element instanceof HTMLInputElement && element.type === 'color')) {
25738
+ element.addEventListener('change', apply);
25739
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25740
+ }
25741
+ else {
25742
+ // Text/number inputs: apply on blur or Enter
25743
+ element.addEventListener('blur', apply);
25744
+ const keyHandler = (e) => {
25745
+ if (e.key === 'Enter') {
25746
+ e.preventDefault();
25747
+ apply();
25748
+ }
25749
+ };
25750
+ element.addEventListener('keydown', keyHandler);
25751
+ this.eventCleanup.push(() => {
25752
+ element.removeEventListener('blur', apply);
25753
+ element.removeEventListener('keydown', keyHandler);
25754
+ });
25755
+ }
25756
+ }
25757
+ /**
25758
+ * Add immediate apply listener for checkbox inputs.
25759
+ */
25760
+ addCheckboxListener(element, handler) {
25761
+ const apply = () => handler(element.checked);
25762
+ element.addEventListener('change', apply);
25763
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25764
+ }
25765
+ /**
25766
+ * Add button click handler with focus steal prevention.
25767
+ */
25768
+ addButtonListener(button, handler) {
25769
+ // Prevent focus steal on mousedown
25770
+ const preventFocus = (e) => {
25771
+ e.preventDefault();
25772
+ this.saveEditorContext();
25773
+ };
25774
+ button.addEventListener('mousedown', preventFocus);
25775
+ button.addEventListener('click', handler);
25776
+ this.eventCleanup.push(() => {
25777
+ button.removeEventListener('mousedown', preventFocus);
25778
+ button.removeEventListener('click', handler);
25779
+ });
25780
+ }
25781
+ /**
25782
+ * Save editor context before UI elements steal focus.
25783
+ */
25784
+ saveEditorContext() {
25785
+ if (this.editor) {
25786
+ this.editor.saveEditingContext();
25787
+ }
25788
+ }
25789
+ /**
25790
+ * Final createElement that wraps content in pane structure.
25791
+ * Content-only, no title bar.
25792
+ */
25793
+ createElement() {
25794
+ const wrapper = document.createElement('div');
25795
+ wrapper.className = 'pc-pane';
25796
+ if (this.className) {
25797
+ wrapper.classList.add(this.className);
25798
+ }
25799
+ wrapper.setAttribute('data-pane-id', this.id);
25800
+ const content = this.createContent();
25801
+ wrapper.appendChild(content);
25802
+ return wrapper;
25803
+ }
25804
+ }
25805
+
25806
+ /**
25807
+ * DocumentInfoPane - Read-only document information display.
25808
+ *
25809
+ * Shows:
25810
+ * - Page count
25811
+ * - Page size
25812
+ * - Page orientation
25813
+ */
25814
+ class DocumentInfoPane extends BasePane {
25815
+ constructor(id = 'document-info') {
25816
+ super(id, { className: 'pc-pane-document-info' });
25817
+ this.pageCountEl = null;
25818
+ this.pageSizeEl = null;
25819
+ this.pageOrientationEl = null;
25820
+ }
25821
+ attach(options) {
25822
+ super.attach(options);
25823
+ // Subscribe to document changes
25824
+ if (this.editor) {
25825
+ const updateHandler = () => this.update();
25826
+ this.editor.on('document-changed', updateHandler);
25827
+ this.editor.on('page-added', updateHandler);
25828
+ this.editor.on('page-removed', updateHandler);
25829
+ this.eventCleanup.push(() => {
25830
+ this.editor?.off('document-changed', updateHandler);
25831
+ this.editor?.off('page-added', updateHandler);
25832
+ this.editor?.off('page-removed', updateHandler);
25833
+ });
25834
+ // Initial update
25835
+ this.update();
25836
+ }
25837
+ }
25838
+ createContent() {
25839
+ const container = document.createElement('div');
25840
+ container.className = 'pc-pane-label-value-grid';
25841
+ // Page count
25842
+ container.appendChild(this.createLabel('Pages:'));
25843
+ this.pageCountEl = this.createValue('0');
25844
+ container.appendChild(this.pageCountEl);
25845
+ container.appendChild(this.createSpacer());
25846
+ // Page size
25847
+ container.appendChild(this.createLabel('Size:'));
25848
+ this.pageSizeEl = this.createValue('-');
25849
+ container.appendChild(this.pageSizeEl);
25850
+ container.appendChild(this.createSpacer());
25851
+ // Page orientation
25852
+ container.appendChild(this.createLabel('Orientation:'));
25853
+ this.pageOrientationEl = this.createValue('-');
25854
+ container.appendChild(this.pageOrientationEl);
25855
+ container.appendChild(this.createSpacer());
25856
+ return container;
25857
+ }
25858
+ createLabel(text) {
25859
+ const label = document.createElement('span');
25860
+ label.className = 'pc-pane-label pc-pane-margin-label';
25861
+ label.textContent = text;
25862
+ return label;
25863
+ }
25864
+ createValue(text) {
25865
+ const value = document.createElement('span');
25866
+ value.className = 'pc-pane-info-value';
25867
+ value.textContent = text;
25868
+ return value;
25869
+ }
25870
+ createSpacer() {
25871
+ return document.createElement('div');
25872
+ }
25873
+ /**
25874
+ * Update the displayed information from the editor.
25875
+ */
25876
+ update() {
25877
+ if (!this.editor)
25878
+ return;
25879
+ const doc = this.editor.getDocument();
25880
+ if (this.pageCountEl) {
25881
+ this.pageCountEl.textContent = doc.pages.length.toString();
25882
+ }
25883
+ if (this.pageSizeEl && doc.settings) {
25884
+ this.pageSizeEl.textContent = doc.settings.pageSize;
25885
+ }
25886
+ if (this.pageOrientationEl && doc.settings) {
25887
+ const orientation = doc.settings.pageOrientation;
25888
+ this.pageOrientationEl.textContent =
25889
+ orientation.charAt(0).toUpperCase() + orientation.slice(1);
25890
+ }
25891
+ }
25892
+ }
25893
+
25894
+ /**
25895
+ * ViewSettingsPane - Toggle buttons for view options.
25896
+ *
25897
+ * Toggles:
25898
+ * - Rulers (requires external callback since rulers are optional controls)
25899
+ * - Control characters
25900
+ * - Margin lines
25901
+ * - Grid
25902
+ */
25903
+ class ViewSettingsPane extends BasePane {
25904
+ constructor(id = 'view-settings', options = {}) {
25905
+ super(id, { className: 'pc-pane-view-settings', ...options });
25906
+ this.rulersBtn = null;
25907
+ this.controlCharsBtn = null;
25908
+ this.marginLinesBtn = null;
25909
+ this.gridBtn = null;
25910
+ this.onToggleRulers = options.onToggleRulers;
25911
+ this.rulersVisible = options.rulersVisible ?? true;
25912
+ }
25913
+ attach(options) {
25914
+ super.attach(options);
25915
+ // Subscribe to editor events
25916
+ if (this.editor) {
25917
+ const updateHandler = () => this.updateButtonStates();
25918
+ this.editor.on('grid-changed', updateHandler);
25919
+ this.editor.on('margin-lines-changed', updateHandler);
25920
+ this.editor.on('control-characters-changed', updateHandler);
25921
+ this.eventCleanup.push(() => {
25922
+ this.editor?.off('grid-changed', updateHandler);
25923
+ this.editor?.off('margin-lines-changed', updateHandler);
25924
+ this.editor?.off('control-characters-changed', updateHandler);
25925
+ });
25926
+ // Initial state
25927
+ this.updateButtonStates();
25928
+ }
25929
+ }
25930
+ createContent() {
25931
+ const container = document.createElement('div');
25932
+ container.className = 'pc-pane-button-group pc-pane-view-toggles';
25933
+ // Rulers toggle (only if callback provided)
25934
+ if (this.onToggleRulers) {
25935
+ this.rulersBtn = this.createToggleButton('Rulers', this.rulersVisible);
25936
+ this.addButtonListener(this.rulersBtn, () => this.toggleRulers());
25937
+ container.appendChild(this.rulersBtn);
25938
+ }
25939
+ // Control characters toggle
25940
+ this.controlCharsBtn = this.createToggleButton('Control Chars', false);
25941
+ this.addButtonListener(this.controlCharsBtn, () => this.toggleControlChars());
25942
+ container.appendChild(this.controlCharsBtn);
25943
+ // Margin lines toggle
25944
+ this.marginLinesBtn = this.createToggleButton('Margin Lines', true);
25945
+ this.addButtonListener(this.marginLinesBtn, () => this.toggleMarginLines());
25946
+ container.appendChild(this.marginLinesBtn);
25947
+ // Grid toggle
25948
+ this.gridBtn = this.createToggleButton('Grid', true);
25949
+ this.addButtonListener(this.gridBtn, () => this.toggleGrid());
25950
+ container.appendChild(this.gridBtn);
25951
+ return container;
25952
+ }
25953
+ createToggleButton(label, active) {
25954
+ const button = document.createElement('button');
25955
+ button.type = 'button';
25956
+ button.className = 'pc-pane-toggle';
25957
+ if (active) {
25958
+ button.classList.add('pc-pane-toggle--active');
25959
+ }
25960
+ button.textContent = label;
25961
+ button.title = `Toggle ${label}`;
25962
+ return button;
25963
+ }
25964
+ toggleRulers() {
25965
+ if (this.onToggleRulers) {
25966
+ this.onToggleRulers();
25967
+ this.rulersVisible = !this.rulersVisible;
25968
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', this.rulersVisible);
25969
+ }
25970
+ }
25971
+ toggleControlChars() {
25972
+ if (!this.editor)
25973
+ return;
25974
+ const current = this.editor.getShowControlCharacters();
25975
+ this.editor.setShowControlCharacters(!current);
25976
+ }
25977
+ toggleMarginLines() {
25978
+ if (!this.editor)
25979
+ return;
25980
+ const current = this.editor.getShowMarginLines();
25981
+ this.editor.setShowMarginLines(!current);
25982
+ }
25983
+ toggleGrid() {
25984
+ if (!this.editor)
25985
+ return;
25986
+ const current = this.editor.getShowGrid();
25987
+ this.editor.setShowGrid(!current);
25988
+ }
25989
+ updateButtonStates() {
25990
+ if (!this.editor)
25991
+ return;
25992
+ if (this.controlCharsBtn) {
25993
+ this.controlCharsBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowControlCharacters());
25994
+ }
25995
+ if (this.marginLinesBtn) {
25996
+ this.marginLinesBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowMarginLines());
25997
+ }
25998
+ if (this.gridBtn) {
25999
+ this.gridBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowGrid());
26000
+ }
26001
+ }
26002
+ /**
26003
+ * Update ruler button state externally (since rulers are external controls).
26004
+ */
26005
+ setRulersVisible(visible) {
26006
+ this.rulersVisible = visible;
26007
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', visible);
26008
+ }
26009
+ /**
26010
+ * Update the pane from current editor state.
26011
+ */
26012
+ update() {
26013
+ this.updateButtonStates();
26014
+ }
26015
+ }
26016
+
26017
+ /**
26018
+ * DocumentSettingsPane - Edit margins, page size, and orientation.
26019
+ *
26020
+ * Uses the PCEditor public API:
26021
+ * - editor.getDocumentSettings()
26022
+ * - editor.updateDocumentSettings()
26023
+ */
26024
+ class DocumentSettingsPane extends BasePane {
26025
+ constructor(id = 'document-settings') {
26026
+ super(id, { className: 'pc-pane-document-settings' });
26027
+ this.marginTopInput = null;
26028
+ this.marginRightInput = null;
26029
+ this.marginBottomInput = null;
26030
+ this.marginLeftInput = null;
26031
+ this.pageSizeSelect = null;
26032
+ this.orientationSelect = null;
26033
+ }
26034
+ attach(options) {
26035
+ super.attach(options);
26036
+ // Load current settings
26037
+ if (this.editor) {
26038
+ this.loadSettings();
26039
+ // Subscribe to document changes
26040
+ const updateHandler = () => this.loadSettings();
26041
+ this.editor.on('document-changed', updateHandler);
26042
+ this.eventCleanup.push(() => {
26043
+ this.editor?.off('document-changed', updateHandler);
26044
+ });
26045
+ }
26046
+ }
26047
+ createContent() {
26048
+ const container = document.createElement('div');
26049
+ // Margins section
26050
+ const marginsSection = this.createSection('Margins (mm)');
26051
+ // Five-column grid: label, edit, label, edit, stretch
26052
+ const marginsGrid = document.createElement('div');
26053
+ marginsGrid.className = 'pc-pane-margins-grid-5col';
26054
+ this.marginTopInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26055
+ this.marginRightInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26056
+ this.marginBottomInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26057
+ this.marginLeftInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
26058
+ // Apply margins on blur
26059
+ const applyMargins = () => this.applyMargins();
26060
+ this.marginTopInput.addEventListener('blur', applyMargins);
26061
+ this.marginRightInput.addEventListener('blur', applyMargins);
26062
+ this.marginBottomInput.addEventListener('blur', applyMargins);
26063
+ this.marginLeftInput.addEventListener('blur', applyMargins);
26064
+ this.eventCleanup.push(() => {
26065
+ this.marginTopInput?.removeEventListener('blur', applyMargins);
26066
+ this.marginRightInput?.removeEventListener('blur', applyMargins);
26067
+ this.marginBottomInput?.removeEventListener('blur', applyMargins);
26068
+ this.marginLeftInput?.removeEventListener('blur', applyMargins);
26069
+ });
26070
+ // Row 1: Top / Right
26071
+ const topLabel = this.createMarginLabel('Top:');
26072
+ const rightLabel = this.createMarginLabel('Right:');
26073
+ marginsGrid.appendChild(topLabel);
26074
+ marginsGrid.appendChild(this.marginTopInput);
26075
+ marginsGrid.appendChild(rightLabel);
26076
+ marginsGrid.appendChild(this.marginRightInput);
26077
+ marginsGrid.appendChild(this.createSpacer());
26078
+ // Row 2: Bottom / Left
26079
+ const bottomLabel = this.createMarginLabel('Bottom:');
26080
+ const leftLabel = this.createMarginLabel('Left:');
26081
+ marginsGrid.appendChild(bottomLabel);
26082
+ marginsGrid.appendChild(this.marginBottomInput);
26083
+ marginsGrid.appendChild(leftLabel);
26084
+ marginsGrid.appendChild(this.marginLeftInput);
26085
+ marginsGrid.appendChild(this.createSpacer());
26086
+ marginsSection.appendChild(marginsGrid);
26087
+ container.appendChild(marginsSection);
26088
+ // Page settings section using label-value grid: label, value, stretch
26089
+ const pageSection = this.createSection();
26090
+ const pageGrid = document.createElement('div');
26091
+ pageGrid.className = 'pc-pane-label-value-grid';
26092
+ // Page Size
26093
+ this.pageSizeSelect = this.createSelect([
26094
+ { value: 'A4', label: 'A4' },
26095
+ { value: 'Letter', label: 'Letter' },
26096
+ { value: 'Legal', label: 'Legal' },
26097
+ { value: 'A3', label: 'A3' }
26098
+ ], 'A4');
26099
+ this.addImmediateApplyListener(this.pageSizeSelect, () => this.applyPageSettings());
26100
+ pageGrid.appendChild(this.createMarginLabel('Page Size:'));
26101
+ pageGrid.appendChild(this.pageSizeSelect);
26102
+ pageGrid.appendChild(this.createSpacer());
26103
+ // Orientation
26104
+ this.orientationSelect = this.createSelect([
26105
+ { value: 'portrait', label: 'Portrait' },
26106
+ { value: 'landscape', label: 'Landscape' }
26107
+ ], 'portrait');
26108
+ this.addImmediateApplyListener(this.orientationSelect, () => this.applyPageSettings());
26109
+ pageGrid.appendChild(this.createMarginLabel('Orientation:'));
26110
+ pageGrid.appendChild(this.orientationSelect);
26111
+ pageGrid.appendChild(this.createSpacer());
26112
+ pageSection.appendChild(pageGrid);
26113
+ container.appendChild(pageSection);
26114
+ return container;
26115
+ }
26116
+ createMarginLabel(text) {
26117
+ const label = document.createElement('label');
26118
+ label.className = 'pc-pane-label pc-pane-margin-label';
26119
+ label.textContent = text;
26120
+ return label;
26121
+ }
26122
+ createSpacer() {
26123
+ const spacer = document.createElement('div');
26124
+ return spacer;
26125
+ }
26126
+ loadSettings() {
26127
+ if (!this.editor)
26128
+ return;
26129
+ try {
26130
+ const settings = this.editor.getDocumentSettings();
26131
+ if (this.marginTopInput) {
26132
+ this.marginTopInput.value = settings.margins.top.toString();
26133
+ }
26134
+ if (this.marginRightInput) {
26135
+ this.marginRightInput.value = settings.margins.right.toString();
26136
+ }
26137
+ if (this.marginBottomInput) {
26138
+ this.marginBottomInput.value = settings.margins.bottom.toString();
26139
+ }
26140
+ if (this.marginLeftInput) {
26141
+ this.marginLeftInput.value = settings.margins.left.toString();
26142
+ }
26143
+ if (this.pageSizeSelect) {
26144
+ this.pageSizeSelect.value = settings.pageSize;
26145
+ }
26146
+ if (this.orientationSelect) {
26147
+ this.orientationSelect.value = settings.pageOrientation;
26148
+ }
26149
+ }
26150
+ catch (error) {
26151
+ console.error('Failed to load document settings:', error);
26152
+ }
26153
+ }
26154
+ applyMargins() {
26155
+ if (!this.editor)
26156
+ return;
26157
+ const margins = {
26158
+ top: parseFloat(this.marginTopInput?.value || '20'),
26159
+ right: parseFloat(this.marginRightInput?.value || '20'),
26160
+ bottom: parseFloat(this.marginBottomInput?.value || '20'),
26161
+ left: parseFloat(this.marginLeftInput?.value || '20')
26162
+ };
26163
+ try {
26164
+ this.editor.updateDocumentSettings({ margins });
26165
+ }
26166
+ catch (error) {
26167
+ console.error('Failed to update margins:', error);
26168
+ }
26169
+ }
26170
+ applyPageSettings() {
26171
+ if (!this.editor)
26172
+ return;
26173
+ const settings = {};
26174
+ if (this.pageSizeSelect) {
26175
+ settings.pageSize = this.pageSizeSelect.value;
26176
+ }
26177
+ if (this.orientationSelect) {
26178
+ settings.pageOrientation = this.orientationSelect.value;
26179
+ }
26180
+ try {
26181
+ this.editor.updateDocumentSettings(settings);
26182
+ }
26183
+ catch (error) {
26184
+ console.error('Failed to update page settings:', error);
26185
+ }
26186
+ }
26187
+ /**
26188
+ * Update the pane from current editor state.
26189
+ */
26190
+ update() {
26191
+ this.loadSettings();
26192
+ }
26193
+ }
26194
+
26195
+ /**
26196
+ * MergeDataPane - JSON data input for mail merge/substitution.
26197
+ *
26198
+ * Uses the PCEditor public API:
26199
+ * - editor.applyMergeData()
26200
+ */
26201
+ class MergeDataPane extends BasePane {
26202
+ constructor(id = 'merge-data', options = {}) {
26203
+ super(id, { className: 'pc-pane-merge-data', ...options });
26204
+ this.textarea = null;
26205
+ this.errorHint = null;
26206
+ this.initialData = options.initialData;
26207
+ this.placeholder = options.placeholder || '{"customerName": "John Doe", "orderNumber": "12345"}';
26208
+ this.rows = options.rows ?? 10;
26209
+ this.onApply = options.onApply;
26210
+ }
26211
+ createContent() {
26212
+ const container = document.createElement('div');
26213
+ // Textarea for JSON
26214
+ const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
26215
+ container.appendChild(textareaGroup);
26216
+ // Error hint (hidden by default)
26217
+ this.errorHint = this.createHint('');
26218
+ this.errorHint.style.display = 'none';
26219
+ this.errorHint.style.color = '#dc3545';
26220
+ container.appendChild(this.errorHint);
26221
+ // Apply button
26222
+ const applyBtn = this.createButton('Apply Merge Data', { variant: 'primary' });
26223
+ this.addButtonListener(applyBtn, () => this.applyMergeData());
26224
+ container.appendChild(applyBtn);
26225
+ return container;
26226
+ }
26227
+ createTextarea() {
26228
+ this.textarea = document.createElement('textarea');
26229
+ this.textarea.className = 'pc-pane-textarea pc-pane-merge-data-input';
26230
+ this.textarea.rows = this.rows;
26231
+ this.textarea.placeholder = this.placeholder;
26232
+ this.textarea.spellcheck = false;
26233
+ if (this.initialData) {
26234
+ this.textarea.value = JSON.stringify(this.initialData, null, 2);
26235
+ }
26236
+ // Clear error on input
26237
+ this.textarea.addEventListener('input', () => {
26238
+ if (this.errorHint) {
26239
+ this.errorHint.style.display = 'none';
26240
+ }
26241
+ });
26242
+ return this.textarea;
26243
+ }
26244
+ applyMergeData() {
26245
+ if (!this.editor || !this.textarea)
26246
+ return;
26247
+ try {
26248
+ const mergeData = JSON.parse(this.textarea.value);
26249
+ this.editor.applyMergeData(mergeData);
26250
+ if (this.errorHint) {
26251
+ this.errorHint.style.display = 'none';
26252
+ }
26253
+ this.onApply?.(true);
26254
+ }
26255
+ catch (error) {
26256
+ const err = error instanceof Error ? error : new Error(String(error));
26257
+ if (this.errorHint) {
26258
+ if (error instanceof SyntaxError) {
26259
+ this.errorHint.textContent = 'Invalid JSON syntax';
26260
+ }
26261
+ else {
26262
+ this.errorHint.textContent = err.message;
26263
+ }
26264
+ this.errorHint.style.display = 'block';
26265
+ }
26266
+ this.onApply?.(false, err);
26267
+ }
26268
+ }
26269
+ /**
26270
+ * Get the current JSON data from the textarea.
26271
+ * Returns null if the JSON is invalid.
26272
+ */
26273
+ getData() {
26274
+ if (!this.textarea)
26275
+ return null;
26276
+ try {
26277
+ return JSON.parse(this.textarea.value);
26278
+ }
26279
+ catch {
26280
+ return null;
26281
+ }
26282
+ }
26283
+ /**
26284
+ * Set the JSON data in the textarea.
26285
+ */
26286
+ setData(data) {
26287
+ if (this.textarea) {
26288
+ this.textarea.value = JSON.stringify(data, null, 2);
26289
+ if (this.errorHint) {
26290
+ this.errorHint.style.display = 'none';
26291
+ }
26292
+ }
26293
+ }
26294
+ /**
26295
+ * Update the pane (no-op for MergeDataPane as it doesn't track editor state).
26296
+ */
26297
+ update() {
26298
+ // MergeDataPane doesn't need to update from editor state
26299
+ }
26300
+ }
26301
+
26302
+ /**
26303
+ * FormattingPane - Text formatting controls.
26304
+ *
26305
+ * Controls:
26306
+ * - Bold/Italic toggles
26307
+ * - Alignment buttons (left, center, right, justify)
26308
+ * - List buttons (bullet, numbered, indent, outdent)
26309
+ * - Font family/size dropdowns
26310
+ * - Text color and highlight color pickers
26311
+ *
26312
+ * Uses the PCEditor public API:
26313
+ * - editor.getUnifiedFormattingAtCursor()
26314
+ * - editor.applyFormattingWithFallback()
26315
+ * - editor.setPendingFormatting()
26316
+ * - editor.getSavedOrCurrentSelection()
26317
+ * - editor.getUnifiedAlignmentAtCursor()
26318
+ * - editor.setUnifiedAlignment()
26319
+ * - editor.toggleBulletList()
26320
+ * - editor.toggleNumberedList()
26321
+ * - editor.indentParagraph()
26322
+ * - editor.outdentParagraph()
26323
+ * - editor.getListFormatting()
26324
+ */
26325
+ const DEFAULT_FONT_FAMILIES = [
26326
+ 'Arial',
26327
+ 'Times New Roman',
26328
+ 'Georgia',
26329
+ 'Verdana',
26330
+ 'Courier New'
26331
+ ];
26332
+ const DEFAULT_FONT_SIZES = [10, 12, 14, 16, 18, 20, 24, 28, 32, 36];
26333
+ class FormattingPane extends BasePane {
26334
+ constructor(id = 'formatting', options = {}) {
26335
+ super(id, { className: 'pc-pane-formatting', ...options });
26336
+ // Style toggles
26337
+ this.boldBtn = null;
26338
+ this.italicBtn = null;
26339
+ // Alignment buttons
26340
+ this.alignLeftBtn = null;
26341
+ this.alignCenterBtn = null;
26342
+ this.alignRightBtn = null;
26343
+ this.alignJustifyBtn = null;
26344
+ // List buttons
26345
+ this.bulletListBtn = null;
26346
+ this.numberedListBtn = null;
26347
+ this.indentBtn = null;
26348
+ this.outdentBtn = null;
26349
+ // Font controls
26350
+ this.fontFamilySelect = null;
26351
+ this.fontSizeSelect = null;
26352
+ this.colorInput = null;
26353
+ this.highlightInput = null;
26354
+ this.fontFamilies = options.fontFamilies ?? DEFAULT_FONT_FAMILIES;
26355
+ this.fontSizes = options.fontSizes ?? DEFAULT_FONT_SIZES;
26356
+ }
26357
+ attach(options) {
26358
+ super.attach(options);
26359
+ if (this.editor) {
26360
+ // Update on cursor/selection changes
26361
+ const updateHandler = () => this.updateFromEditor();
26362
+ this.editor.on('cursor-changed', updateHandler);
26363
+ this.editor.on('selection-changed', updateHandler);
26364
+ this.editor.on('text-changed', updateHandler);
26365
+ this.editor.on('formatting-changed', updateHandler);
26366
+ this.eventCleanup.push(() => {
26367
+ this.editor?.off('cursor-changed', updateHandler);
26368
+ this.editor?.off('selection-changed', updateHandler);
26369
+ this.editor?.off('text-changed', updateHandler);
26370
+ this.editor?.off('formatting-changed', updateHandler);
26371
+ });
26372
+ // Initial update
26373
+ this.updateFromEditor();
26374
+ }
26375
+ }
26376
+ createContent() {
26377
+ const container = document.createElement('div');
26378
+ // Style section (Bold, Italic)
26379
+ const styleSection = this.createSection('Style');
26380
+ const styleGroup = this.createButtonGroup();
26381
+ this.boldBtn = this.createButton('B');
26382
+ this.boldBtn.title = 'Bold';
26383
+ this.boldBtn.style.fontWeight = 'bold';
26384
+ this.addButtonListener(this.boldBtn, () => this.toggleBold());
26385
+ this.italicBtn = this.createButton('I');
26386
+ this.italicBtn.title = 'Italic';
26387
+ this.italicBtn.style.fontStyle = 'italic';
26388
+ this.addButtonListener(this.italicBtn, () => this.toggleItalic());
26389
+ styleGroup.appendChild(this.boldBtn);
26390
+ styleGroup.appendChild(this.italicBtn);
26391
+ styleSection.appendChild(styleGroup);
26392
+ container.appendChild(styleSection);
26393
+ // Alignment section
26394
+ const alignSection = this.createSection('Alignment');
26395
+ const alignGroup = this.createButtonGroup();
26396
+ this.alignLeftBtn = this.createButton('');
26397
+ this.alignLeftBtn.title = 'Align Left';
26398
+ this.alignLeftBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-left');
26399
+ this.addButtonListener(this.alignLeftBtn, () => this.setAlignment('left'));
26400
+ this.alignCenterBtn = this.createButton('');
26401
+ this.alignCenterBtn.title = 'Center';
26402
+ this.alignCenterBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-center');
26403
+ this.addButtonListener(this.alignCenterBtn, () => this.setAlignment('center'));
26404
+ this.alignRightBtn = this.createButton('');
26405
+ this.alignRightBtn.title = 'Align Right';
26406
+ this.alignRightBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-right');
26407
+ this.addButtonListener(this.alignRightBtn, () => this.setAlignment('right'));
26408
+ this.alignJustifyBtn = this.createButton('');
26409
+ this.alignJustifyBtn.title = 'Justify';
26410
+ this.alignJustifyBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-justify');
26411
+ this.addButtonListener(this.alignJustifyBtn, () => this.setAlignment('justify'));
26412
+ alignGroup.appendChild(this.alignLeftBtn);
26413
+ alignGroup.appendChild(this.alignCenterBtn);
26414
+ alignGroup.appendChild(this.alignRightBtn);
26415
+ alignGroup.appendChild(this.alignJustifyBtn);
26416
+ alignSection.appendChild(alignGroup);
26417
+ container.appendChild(alignSection);
26418
+ // Lists section
26419
+ const listsSection = this.createSection('Lists');
26420
+ const listsGroup = this.createButtonGroup();
26421
+ this.bulletListBtn = this.createButton('\u2022'); // •
26422
+ this.bulletListBtn.title = 'Bullet List';
26423
+ this.addButtonListener(this.bulletListBtn, () => this.toggleBulletList());
26424
+ this.numberedListBtn = this.createButton('1.');
26425
+ this.numberedListBtn.title = 'Numbered List';
26426
+ this.addButtonListener(this.numberedListBtn, () => this.toggleNumberedList());
26427
+ this.indentBtn = this.createButton('\u2192'); // →
26428
+ this.indentBtn.title = 'Increase Indent';
26429
+ this.addButtonListener(this.indentBtn, () => this.indent());
26430
+ this.outdentBtn = this.createButton('\u2190'); // ←
26431
+ this.outdentBtn.title = 'Decrease Indent';
26432
+ this.addButtonListener(this.outdentBtn, () => this.outdent());
26433
+ listsGroup.appendChild(this.bulletListBtn);
26434
+ listsGroup.appendChild(this.numberedListBtn);
26435
+ listsGroup.appendChild(this.indentBtn);
26436
+ listsGroup.appendChild(this.outdentBtn);
26437
+ listsSection.appendChild(listsGroup);
26438
+ container.appendChild(listsSection);
26439
+ // Font section
26440
+ const fontSection = this.createSection('Font');
26441
+ this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26442
+ this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26443
+ fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
26444
+ this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26445
+ this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26446
+ fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
26447
+ container.appendChild(fontSection);
26448
+ // Color section
26449
+ const colorSection = this.createSection('Color');
26450
+ const colorRow = this.createRow();
26451
+ const colorGroup = document.createElement('div');
26452
+ this.colorInput = this.createColorInput('#000000');
26453
+ this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26454
+ colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26455
+ colorRow.appendChild(colorGroup);
26456
+ const highlightGroup = document.createElement('div');
26457
+ this.highlightInput = this.createColorInput('#ffff00');
26458
+ this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26459
+ const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
26460
+ const clearHighlightBtn = this.createButton('Clear');
26461
+ clearHighlightBtn.className = 'pc-pane-button';
26462
+ clearHighlightBtn.style.marginLeft = '4px';
26463
+ this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26464
+ highlightForm.appendChild(clearHighlightBtn);
26465
+ highlightGroup.appendChild(highlightForm);
26466
+ colorRow.appendChild(highlightGroup);
26467
+ colorSection.appendChild(colorRow);
26468
+ container.appendChild(colorSection);
26469
+ return container;
26470
+ }
26471
+ updateFromEditor() {
26472
+ if (!this.editor)
26473
+ return;
26474
+ // Get formatting at cursor
26475
+ const formatting = this.editor.getUnifiedFormattingAtCursor();
26476
+ if (formatting) {
26477
+ // Update bold button
26478
+ this.boldBtn?.classList.toggle('pc-pane-button--active', formatting.fontWeight === 'bold');
26479
+ // Update italic button
26480
+ this.italicBtn?.classList.toggle('pc-pane-button--active', formatting.fontStyle === 'italic');
26481
+ // Update font family
26482
+ if (this.fontFamilySelect && formatting.fontFamily) {
26483
+ this.fontFamilySelect.value = formatting.fontFamily;
26484
+ }
26485
+ // Update font size
26486
+ if (this.fontSizeSelect && formatting.fontSize) {
26487
+ this.fontSizeSelect.value = formatting.fontSize.toString();
26488
+ }
26489
+ // Update color
26490
+ if (this.colorInput && formatting.color) {
26491
+ this.colorInput.value = formatting.color;
26492
+ }
26493
+ // Update highlight
26494
+ if (this.highlightInput && formatting.backgroundColor) {
26495
+ this.highlightInput.value = formatting.backgroundColor;
26496
+ }
26497
+ }
26498
+ // Update alignment buttons
26499
+ const alignment = this.editor.getUnifiedAlignmentAtCursor();
26500
+ this.updateAlignmentButtons(alignment);
26501
+ // Update list buttons
26502
+ this.updateListButtons();
26503
+ }
26504
+ updateAlignmentButtons(alignment) {
26505
+ const buttons = [
26506
+ { btn: this.alignLeftBtn, align: 'left' },
26507
+ { btn: this.alignCenterBtn, align: 'center' },
26508
+ { btn: this.alignRightBtn, align: 'right' },
26509
+ { btn: this.alignJustifyBtn, align: 'justify' }
26510
+ ];
26511
+ for (const { btn, align } of buttons) {
26512
+ btn?.classList.toggle('pc-pane-button--active', align === alignment);
26513
+ }
26514
+ }
26515
+ updateListButtons() {
26516
+ if (!this.editor)
26517
+ return;
26518
+ try {
26519
+ const listFormatting = this.editor.getListFormatting();
26520
+ if (listFormatting) {
26521
+ this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26522
+ this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26523
+ }
26524
+ }
26525
+ catch {
26526
+ // No text editing active
26527
+ }
26528
+ }
26529
+ getSelection() {
26530
+ if (!this.editor)
26531
+ return null;
26532
+ return this.editor.getSavedOrCurrentSelection();
26533
+ }
26534
+ applyFormatting(formatting) {
26535
+ if (!this.editor)
26536
+ return;
26537
+ const selection = this.getSelection();
26538
+ try {
26539
+ if (selection) {
26540
+ this.editor.applyFormattingWithFallback(selection.start, selection.end, formatting);
26541
+ }
26542
+ else {
26543
+ this.editor.setPendingFormatting(formatting);
26544
+ }
26545
+ this.editor.clearSavedEditingContext();
26546
+ this.updateFromEditor();
26547
+ this.editor.enableTextInput();
26548
+ }
26549
+ catch (error) {
26550
+ console.error('Formatting error:', error);
26551
+ }
26552
+ }
26553
+ toggleBold() {
26554
+ const isActive = this.boldBtn?.classList.contains('pc-pane-button--active');
26555
+ this.applyFormatting({ fontWeight: isActive ? 'normal' : 'bold' });
26556
+ }
26557
+ toggleItalic() {
26558
+ const isActive = this.italicBtn?.classList.contains('pc-pane-button--active');
26559
+ this.applyFormatting({ fontStyle: isActive ? 'normal' : 'italic' });
26560
+ }
26561
+ applyFontFamily() {
26562
+ if (this.fontFamilySelect) {
26563
+ this.applyFormatting({ fontFamily: this.fontFamilySelect.value });
26564
+ }
26565
+ }
26566
+ applyFontSize() {
26567
+ if (this.fontSizeSelect) {
26568
+ this.applyFormatting({ fontSize: parseInt(this.fontSizeSelect.value, 10) });
26569
+ }
26570
+ }
26571
+ applyTextColor() {
26572
+ if (this.colorInput) {
26573
+ this.applyFormatting({ color: this.colorInput.value });
26574
+ }
26575
+ }
26576
+ applyHighlight() {
26577
+ if (this.highlightInput) {
26578
+ this.applyFormatting({ backgroundColor: this.highlightInput.value });
26579
+ }
26580
+ }
26581
+ clearHighlight() {
26582
+ this.applyFormatting({ backgroundColor: undefined });
26583
+ }
26584
+ setAlignment(alignment) {
26585
+ if (!this.editor)
26586
+ return;
26587
+ try {
26588
+ this.editor.setUnifiedAlignment(alignment);
26589
+ this.updateAlignmentButtons(alignment);
26590
+ }
26591
+ catch (error) {
26592
+ console.error('Alignment error:', error);
26593
+ }
26594
+ }
26595
+ toggleBulletList() {
26596
+ if (!this.editor)
26597
+ return;
26598
+ try {
26599
+ this.editor.toggleBulletList();
26600
+ this.updateListButtons();
26601
+ }
26602
+ catch (error) {
26603
+ console.error('Bullet list error:', error);
26604
+ }
26605
+ }
26606
+ toggleNumberedList() {
26607
+ if (!this.editor)
26608
+ return;
26609
+ try {
26610
+ this.editor.toggleNumberedList();
26611
+ this.updateListButtons();
26612
+ }
26613
+ catch (error) {
26614
+ console.error('Numbered list error:', error);
26615
+ }
26616
+ }
26617
+ indent() {
26618
+ if (!this.editor)
26619
+ return;
26620
+ try {
26621
+ this.editor.indentParagraph();
26622
+ this.updateListButtons();
26623
+ }
26624
+ catch (error) {
26625
+ console.error('Indent error:', error);
26626
+ }
26627
+ }
26628
+ outdent() {
26629
+ if (!this.editor)
26630
+ return;
26631
+ try {
26632
+ this.editor.outdentParagraph();
26633
+ this.updateListButtons();
26634
+ }
26635
+ catch (error) {
26636
+ console.error('Outdent error:', error);
26637
+ }
26638
+ }
26639
+ /**
26640
+ * Update the pane from current editor state.
26641
+ */
26642
+ update() {
26643
+ this.updateFromEditor();
26644
+ }
26645
+ }
26646
+
26647
+ /**
26648
+ * HyperlinkPane - Edit hyperlink URL and title.
26649
+ *
26650
+ * This pane is shown when a hyperlink is selected/cursor is in a hyperlink.
26651
+ *
26652
+ * Uses the PCEditor public API:
26653
+ * - editor.getHyperlinkAt()
26654
+ * - editor.updateHyperlink()
26655
+ * - editor.removeHyperlink()
26656
+ * - editor.getCursorPosition()
26657
+ */
26658
+ class HyperlinkPane extends BasePane {
26659
+ constructor(id = 'hyperlink', options = {}) {
26660
+ super(id, { className: 'pc-pane-hyperlink', ...options });
26661
+ this.urlInput = null;
26662
+ this.titleInput = null;
26663
+ this.rangeHint = null;
26664
+ this.currentHyperlink = null;
26665
+ this._isUpdating = false;
26666
+ this.onApply = options.onApply;
26667
+ this.onRemove = options.onRemove;
26668
+ }
26669
+ attach(options) {
26670
+ super.attach(options);
26671
+ if (this.editor) {
26672
+ // Update on cursor changes
26673
+ const updateHandler = () => this.updateFromCursor();
26674
+ this.editor.on('cursor-changed', updateHandler);
26675
+ this.editor.on('selection-changed', updateHandler);
26676
+ this.eventCleanup.push(() => {
26677
+ this.editor?.off('cursor-changed', updateHandler);
26678
+ this.editor?.off('selection-changed', updateHandler);
26679
+ });
26680
+ // Initial update
26681
+ this.updateFromCursor();
26682
+ }
26683
+ }
26684
+ createContent() {
26685
+ const container = document.createElement('div');
26686
+ // URL input
26687
+ this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26688
+ container.appendChild(this.createFormGroup('URL', this.urlInput));
26689
+ // Title input
26690
+ this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26691
+ container.appendChild(this.createFormGroup('Title', this.titleInput));
26692
+ // Apply button
26693
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26694
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26695
+ container.appendChild(applyBtn);
26696
+ // Remove button
26697
+ const removeBtn = this.createButton('Remove Link', { variant: 'danger' });
26698
+ removeBtn.style.marginTop = '0.5rem';
26699
+ this.addButtonListener(removeBtn, () => this.removeHyperlink());
26700
+ container.appendChild(removeBtn);
26701
+ // Range hint
26702
+ this.rangeHint = this.createHint('');
26703
+ container.appendChild(this.rangeHint);
26704
+ return container;
26705
+ }
26706
+ updateFromCursor() {
26707
+ if (!this.editor || this._isUpdating)
26708
+ return;
26709
+ this._isUpdating = true;
26710
+ try {
26711
+ const cursorPos = this.editor.getCursorPosition();
26712
+ const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26713
+ if (hyperlink) {
26714
+ this.showHyperlink(hyperlink);
26715
+ }
26716
+ else {
26717
+ this.hideHyperlink();
26718
+ }
26719
+ }
26720
+ finally {
26721
+ this._isUpdating = false;
26722
+ }
26723
+ }
26724
+ showHyperlink(hyperlink) {
26725
+ this.currentHyperlink = hyperlink;
26726
+ if (this.urlInput) {
26727
+ this.urlInput.value = hyperlink.url;
26728
+ }
26729
+ if (this.titleInput) {
26730
+ this.titleInput.value = hyperlink.title || '';
26731
+ }
26732
+ if (this.rangeHint) {
26733
+ this.rangeHint.textContent = `Link spans characters ${hyperlink.startIndex} to ${hyperlink.endIndex}`;
26734
+ }
26735
+ // Show the pane
26736
+ this.show();
26737
+ }
26738
+ hideHyperlink() {
26739
+ this.currentHyperlink = null;
26740
+ this.hide();
26741
+ }
26742
+ applyChanges() {
26743
+ if (!this.editor || !this.currentHyperlink)
26744
+ return;
26745
+ try {
26746
+ const url = this.urlInput?.value.trim() || '';
26747
+ const title = this.titleInput?.value.trim() || undefined;
26748
+ if (!url) {
26749
+ this.onApply?.(false, new Error('URL is required'));
26750
+ return;
26751
+ }
26752
+ this.editor.updateHyperlink(this.currentHyperlink.id, { url, title });
26753
+ // Update local reference
26754
+ this.currentHyperlink.url = url;
26755
+ this.currentHyperlink.title = title;
26756
+ this.onApply?.(true);
26757
+ }
26758
+ catch (error) {
26759
+ this.onApply?.(false, error instanceof Error ? error : new Error(String(error)));
26760
+ }
26761
+ }
26762
+ removeHyperlink() {
26763
+ if (!this.editor || !this.currentHyperlink)
26764
+ return;
26765
+ try {
26766
+ this.editor.removeHyperlink(this.currentHyperlink.id);
26767
+ this.hideHyperlink();
26768
+ this.onRemove?.(true);
26769
+ }
26770
+ catch {
26771
+ this.onRemove?.(false);
26772
+ }
26773
+ }
26774
+ /**
26775
+ * Get the currently selected hyperlink.
26776
+ */
26777
+ getCurrentHyperlink() {
26778
+ return this.currentHyperlink;
26779
+ }
26780
+ /**
26781
+ * Check if a hyperlink is currently selected.
26782
+ */
26783
+ hasHyperlink() {
26784
+ return this.currentHyperlink !== null;
26785
+ }
26786
+ /**
26787
+ * Update the pane from current editor state.
26788
+ */
26789
+ update() {
26790
+ this.updateFromCursor();
26791
+ }
26792
+ }
26793
+
26794
+ /**
26795
+ * SubstitutionFieldPane - Edit substitution field properties.
26796
+ *
26797
+ * Shows:
26798
+ * - Field name
26799
+ * - Default value
26800
+ * - Format configuration (value type, number/currency/date formats)
26801
+ *
26802
+ * Uses the PCEditor public API:
26803
+ * - editor.getFieldAt()
26804
+ * - editor.updateField()
26805
+ */
26806
+ class SubstitutionFieldPane extends BasePane {
26807
+ constructor(id = 'substitution-field', options = {}) {
26808
+ super(id, { className: 'pc-pane-substitution-field', ...options });
26809
+ this.fieldNameInput = null;
26810
+ this.fieldDefaultInput = null;
26811
+ this.valueTypeSelect = null;
26812
+ this.numberFormatSelect = null;
26813
+ this.currencyFormatSelect = null;
26814
+ this.dateFormatSelect = null;
26815
+ this.positionHint = null;
26816
+ this.numberFormatGroup = null;
26817
+ this.currencyFormatGroup = null;
26818
+ this.dateFormatGroup = null;
26819
+ this.currentField = null;
26820
+ this.onApplyCallback = options.onApply;
26821
+ }
26822
+ attach(options) {
26823
+ super.attach(options);
26824
+ if (this.editor) {
26825
+ // Listen for field selection events
26826
+ const selectionHandler = (event) => {
26827
+ if (event.type === 'field' && event.field) {
26828
+ this.showField(event.field);
26829
+ }
26830
+ else if (!event.type || event.type !== 'field') ;
26831
+ };
26832
+ this.editor.on('selection-change', selectionHandler);
26833
+ this.eventCleanup.push(() => {
26834
+ this.editor?.off('selection-change', selectionHandler);
26835
+ });
26836
+ }
26837
+ }
26838
+ createContent() {
26839
+ const container = document.createElement('div');
26840
+ // Field name input
26841
+ this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26842
+ container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
26843
+ // Default value input
26844
+ this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26845
+ container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
26846
+ // Value type select
26847
+ this.valueTypeSelect = this.createSelect([
26848
+ { value: '', label: '(None)' },
26849
+ { value: 'number', label: 'Number' },
26850
+ { value: 'currency', label: 'Currency' },
26851
+ { value: 'date', label: 'Date' }
26852
+ ]);
26853
+ this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26854
+ container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
26855
+ // Number format group
26856
+ this.numberFormatGroup = this.createSection();
26857
+ this.numberFormatGroup.style.display = 'none';
26858
+ this.numberFormatSelect = this.createSelect([
26859
+ { value: '0', label: 'Integer (0)' },
26860
+ { value: '0.00', label: 'Two decimals (0.00)' },
26861
+ { value: '0,0', label: 'Thousands separator (0,0)' },
26862
+ { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26863
+ ]);
26864
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
26865
+ container.appendChild(this.numberFormatGroup);
26866
+ // Currency format group
26867
+ this.currencyFormatGroup = this.createSection();
26868
+ this.currencyFormatGroup.style.display = 'none';
26869
+ this.currencyFormatSelect = this.createSelect([
26870
+ { value: 'USD', label: 'USD ($)' },
26871
+ { value: 'EUR', label: 'EUR' },
26872
+ { value: 'GBP', label: 'GBP' },
26873
+ { value: 'JPY', label: 'JPY' }
26874
+ ]);
26875
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
26876
+ container.appendChild(this.currencyFormatGroup);
26877
+ // Date format group
26878
+ this.dateFormatGroup = this.createSection();
26879
+ this.dateFormatGroup.style.display = 'none';
26880
+ this.dateFormatSelect = this.createSelect([
26881
+ { value: 'MMMM D, YYYY', label: 'January 1, 2026' },
26882
+ { value: 'MM/DD/YYYY', label: '01/01/2026' },
26883
+ { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26884
+ { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26885
+ ]);
26886
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
26887
+ container.appendChild(this.dateFormatGroup);
26888
+ // Apply button
26889
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26890
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26891
+ container.appendChild(applyBtn);
26892
+ // Position hint
26893
+ this.positionHint = this.createHint('');
26894
+ container.appendChild(this.positionHint);
26895
+ return container;
26896
+ }
26897
+ updateFormatGroups() {
26898
+ const valueType = this.valueTypeSelect?.value || '';
26899
+ if (this.numberFormatGroup) {
26900
+ this.numberFormatGroup.style.display = valueType === 'number' ? 'block' : 'none';
26901
+ }
26902
+ if (this.currencyFormatGroup) {
26903
+ this.currencyFormatGroup.style.display = valueType === 'currency' ? 'block' : 'none';
26904
+ }
26905
+ if (this.dateFormatGroup) {
26906
+ this.dateFormatGroup.style.display = valueType === 'date' ? 'block' : 'none';
26907
+ }
26908
+ }
26909
+ /**
26910
+ * Show the pane with the given field.
26911
+ */
26912
+ showField(field) {
26913
+ this.currentField = field;
26914
+ if (this.fieldNameInput) {
26915
+ this.fieldNameInput.value = field.fieldName;
26916
+ }
26917
+ if (this.fieldDefaultInput) {
26918
+ this.fieldDefaultInput.value = field.defaultValue || '';
26919
+ }
26920
+ if (this.positionHint) {
26921
+ this.positionHint.textContent = `Field at position ${field.textIndex}`;
26922
+ }
26923
+ // Populate format options
26924
+ if (this.valueTypeSelect) {
26925
+ this.valueTypeSelect.value = field.formatConfig?.valueType || '';
26926
+ }
26927
+ if (this.numberFormatSelect && field.formatConfig?.numberFormat) {
26928
+ this.numberFormatSelect.value = field.formatConfig.numberFormat;
26929
+ }
26930
+ if (this.currencyFormatSelect && field.formatConfig?.currencyFormat) {
26931
+ this.currencyFormatSelect.value = field.formatConfig.currencyFormat;
26932
+ }
26933
+ if (this.dateFormatSelect && field.formatConfig?.dateFormat) {
26934
+ this.dateFormatSelect.value = field.formatConfig.dateFormat;
26935
+ }
26936
+ this.updateFormatGroups();
26937
+ this.show();
26938
+ }
26939
+ /**
26940
+ * Hide the pane and clear the current field.
26941
+ */
26942
+ hideField() {
26943
+ this.currentField = null;
26944
+ this.hide();
26945
+ }
26946
+ applyChanges() {
26947
+ if (!this.editor || !this.currentField) {
26948
+ this.onApplyCallback?.(false, new Error('No field selected'));
26949
+ return;
26950
+ }
26951
+ const fieldName = this.fieldNameInput?.value.trim();
26952
+ if (!fieldName) {
26953
+ this.onApplyCallback?.(false, new Error('Field name cannot be empty'));
26954
+ return;
26955
+ }
26956
+ const updates = {};
26957
+ if (fieldName !== this.currentField.fieldName) {
26958
+ updates.fieldName = fieldName;
26959
+ }
26960
+ const defaultValue = this.fieldDefaultInput?.value || undefined;
26961
+ if (defaultValue !== this.currentField.defaultValue) {
26962
+ updates.defaultValue = defaultValue;
26963
+ }
26964
+ // Build format config
26965
+ const valueType = this.valueTypeSelect?.value;
26966
+ if (valueType) {
26967
+ const formatConfig = {
26968
+ valueType: valueType
26969
+ };
26970
+ if (valueType === 'number' && this.numberFormatSelect?.value) {
26971
+ formatConfig.numberFormat = this.numberFormatSelect.value;
26972
+ }
26973
+ else if (valueType === 'currency' && this.currencyFormatSelect?.value) {
26974
+ formatConfig.currencyFormat = this.currencyFormatSelect.value;
26975
+ }
26976
+ else if (valueType === 'date' && this.dateFormatSelect?.value) {
26977
+ formatConfig.dateFormat = this.dateFormatSelect.value;
26978
+ }
26979
+ updates.formatConfig = formatConfig;
26980
+ }
26981
+ else if (this.currentField.formatConfig) {
26982
+ updates.formatConfig = undefined;
26983
+ }
26984
+ if (Object.keys(updates).length === 0) {
26985
+ return; // No changes
26986
+ }
26987
+ try {
26988
+ const success = this.editor.updateField(this.currentField.textIndex, updates);
26989
+ if (success) {
26990
+ // Update the current field reference
26991
+ this.currentField = this.editor.getFieldAt(this.currentField.textIndex) || null;
26992
+ this.onApplyCallback?.(true);
26993
+ }
26994
+ else {
26995
+ this.onApplyCallback?.(false, new Error('Failed to update field'));
26996
+ }
26997
+ }
26998
+ catch (error) {
26999
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27000
+ }
27001
+ }
27002
+ /**
27003
+ * Get the currently selected field.
27004
+ */
27005
+ getCurrentField() {
27006
+ return this.currentField;
27007
+ }
27008
+ /**
27009
+ * Check if a field is currently selected.
27010
+ */
27011
+ hasField() {
27012
+ return this.currentField !== null;
27013
+ }
27014
+ /**
27015
+ * Update the pane from current editor state.
27016
+ */
27017
+ update() {
27018
+ // Field pane doesn't auto-update - it's driven by selection events
27019
+ }
27020
+ }
27021
+
27022
+ /**
27023
+ * RepeatingSectionPane - Edit repeating section (loop) properties.
27024
+ *
27025
+ * Shows:
27026
+ * - Field path (array property in merge data)
27027
+ * - Position information
27028
+ *
27029
+ * Uses the PCEditor public API:
27030
+ * - editor.getRepeatingSection()
27031
+ * - editor.updateRepeatingSectionFieldPath()
27032
+ * - editor.removeRepeatingSection()
27033
+ */
27034
+ class RepeatingSectionPane extends BasePane {
27035
+ constructor(id = 'repeating-section', options = {}) {
27036
+ super(id, { className: 'pc-pane-repeating-section', ...options });
27037
+ this.fieldPathInput = null;
27038
+ this.positionHint = null;
27039
+ this.currentSection = null;
27040
+ this.onApplyCallback = options.onApply;
27041
+ this.onRemoveCallback = options.onRemove;
27042
+ }
27043
+ attach(options) {
27044
+ super.attach(options);
27045
+ if (this.editor) {
27046
+ // Listen for repeating section selection
27047
+ const selectionHandler = (event) => {
27048
+ if (event.type === 'repeating-section' && event.sectionId) {
27049
+ const section = this.editor?.getRepeatingSection(event.sectionId);
27050
+ if (section) {
27051
+ this.showSection(section);
27052
+ }
27053
+ }
27054
+ };
27055
+ const removedHandler = () => {
27056
+ this.hideSection();
27057
+ };
27058
+ this.editor.on('selection-change', selectionHandler);
27059
+ this.editor.on('repeating-section-removed', removedHandler);
27060
+ this.eventCleanup.push(() => {
27061
+ this.editor?.off('selection-change', selectionHandler);
27062
+ this.editor?.off('repeating-section-removed', removedHandler);
27063
+ });
27064
+ }
27065
+ }
27066
+ createContent() {
27067
+ const container = document.createElement('div');
27068
+ // Field path input
27069
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27070
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
27071
+ hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
27072
+ }));
27073
+ // Apply button
27074
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27075
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27076
+ container.appendChild(applyBtn);
27077
+ // Remove button
27078
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
27079
+ removeBtn.style.marginTop = '0.5rem';
27080
+ this.addButtonListener(removeBtn, () => this.removeSection());
27081
+ container.appendChild(removeBtn);
27082
+ // Position hint
27083
+ this.positionHint = this.createHint('');
27084
+ container.appendChild(this.positionHint);
27085
+ return container;
27086
+ }
27087
+ /**
27088
+ * Show the pane with the given section.
27089
+ */
27090
+ showSection(section) {
27091
+ this.currentSection = section;
27092
+ if (this.fieldPathInput) {
27093
+ this.fieldPathInput.value = section.fieldPath;
27094
+ }
27095
+ if (this.positionHint) {
27096
+ this.positionHint.textContent = `Loop from position ${section.startIndex} to ${section.endIndex}`;
27097
+ }
27098
+ this.show();
27099
+ }
27100
+ /**
27101
+ * Hide the pane and clear the current section.
27102
+ */
27103
+ hideSection() {
27104
+ this.currentSection = null;
27105
+ this.hide();
27106
+ }
27107
+ applyChanges() {
27108
+ if (!this.editor || !this.currentSection) {
27109
+ this.onApplyCallback?.(false, new Error('No section selected'));
27110
+ return;
27111
+ }
27112
+ const fieldPath = this.fieldPathInput?.value.trim();
27113
+ if (!fieldPath) {
27114
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
27115
+ return;
27116
+ }
27117
+ if (fieldPath === this.currentSection.fieldPath) {
27118
+ return; // No changes
27119
+ }
27120
+ try {
27121
+ const success = this.editor.updateRepeatingSectionFieldPath(this.currentSection.id, fieldPath);
27122
+ if (success) {
27123
+ // Update the current section reference
27124
+ this.currentSection = this.editor.getRepeatingSection(this.currentSection.id) || null;
27125
+ if (this.currentSection) {
27126
+ this.showSection(this.currentSection);
27127
+ }
27128
+ this.onApplyCallback?.(true);
27129
+ }
27130
+ else {
27131
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
27132
+ }
27133
+ }
27134
+ catch (error) {
27135
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27136
+ }
27137
+ }
27138
+ removeSection() {
27139
+ if (!this.editor || !this.currentSection)
27140
+ return;
27141
+ try {
27142
+ this.editor.removeRepeatingSection(this.currentSection.id);
27143
+ this.hideSection();
27144
+ this.onRemoveCallback?.(true);
27145
+ }
27146
+ catch {
27147
+ this.onRemoveCallback?.(false);
27148
+ }
27149
+ }
27150
+ /**
27151
+ * Get the currently selected section.
27152
+ */
27153
+ getCurrentSection() {
27154
+ return this.currentSection;
27155
+ }
27156
+ /**
27157
+ * Check if a section is currently selected.
27158
+ */
27159
+ hasSection() {
27160
+ return this.currentSection !== null;
27161
+ }
27162
+ /**
27163
+ * Update the pane from current editor state.
27164
+ */
27165
+ update() {
27166
+ // Section pane doesn't auto-update - it's driven by selection events
27167
+ }
27168
+ }
27169
+
27170
+ /**
27171
+ * TableRowLoopPane - Edit table row loop properties.
27172
+ *
27173
+ * Shows:
27174
+ * - Field path (array property in merge data)
27175
+ * - Row range information
27176
+ *
27177
+ * Uses the TableObject API:
27178
+ * - table.getRowLoop()
27179
+ * - table.updateRowLoopFieldPath()
27180
+ * - table.removeRowLoop()
27181
+ */
27182
+ class TableRowLoopPane extends BasePane {
27183
+ constructor(id = 'table-row-loop', options = {}) {
27184
+ super(id, { className: 'pc-pane-table-row-loop', ...options });
27185
+ this.fieldPathInput = null;
27186
+ this.rangeHint = null;
27187
+ this.currentLoop = null;
27188
+ this.currentTable = null;
27189
+ this.onApplyCallback = options.onApply;
27190
+ this.onRemoveCallback = options.onRemove;
27191
+ }
27192
+ attach(options) {
27193
+ super.attach(options);
27194
+ // Table row loop pane is typically shown manually when a table's row loop is selected
27195
+ // The consumer is responsible for calling showLoop() with the table and loop
27196
+ }
27197
+ createContent() {
27198
+ const container = document.createElement('div');
27199
+ // Field path input
27200
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27201
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
27202
+ hint: 'Path to array in merge data (e.g., "items" or "orders")'
27203
+ }));
27204
+ // Apply button
27205
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27206
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27207
+ container.appendChild(applyBtn);
27208
+ // Remove button
27209
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
27210
+ removeBtn.style.marginTop = '0.5rem';
27211
+ this.addButtonListener(removeBtn, () => this.removeLoop());
27212
+ container.appendChild(removeBtn);
27213
+ // Range hint
27214
+ this.rangeHint = this.createHint('');
27215
+ container.appendChild(this.rangeHint);
27216
+ return container;
27217
+ }
27218
+ /**
27219
+ * Show the pane with the given table and loop.
27220
+ */
27221
+ showLoop(table, loop) {
27222
+ this.currentTable = table;
27223
+ this.currentLoop = loop;
27224
+ if (this.fieldPathInput) {
27225
+ this.fieldPathInput.value = loop.fieldPath;
27226
+ }
27227
+ if (this.rangeHint) {
27228
+ this.rangeHint.textContent = `Rows ${loop.startRowIndex} - ${loop.endRowIndex}`;
27229
+ }
27230
+ this.show();
27231
+ }
27232
+ /**
27233
+ * Hide the pane and clear current loop.
27234
+ */
27235
+ hideLoop() {
27236
+ this.currentTable = null;
27237
+ this.currentLoop = null;
27238
+ this.hide();
27239
+ }
27240
+ applyChanges() {
27241
+ if (!this.currentTable || !this.currentLoop) {
27242
+ this.onApplyCallback?.(false, new Error('No loop selected'));
27243
+ return;
27244
+ }
27245
+ const fieldPath = this.fieldPathInput?.value.trim();
27246
+ if (!fieldPath) {
27247
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
27248
+ return;
27249
+ }
27250
+ if (fieldPath === this.currentLoop.fieldPath) {
27251
+ return; // No changes
27252
+ }
27253
+ try {
27254
+ const success = this.currentTable.updateRowLoopFieldPath(this.currentLoop.id, fieldPath);
27255
+ if (success) {
27256
+ // Update the current loop reference
27257
+ this.currentLoop = this.currentTable.getRowLoop(this.currentLoop.id) || null;
27258
+ if (this.currentLoop) {
27259
+ this.showLoop(this.currentTable, this.currentLoop);
27260
+ }
27261
+ this.onApplyCallback?.(true);
27262
+ }
27263
+ else {
27264
+ this.onApplyCallback?.(false, new Error('Failed to update loop'));
27265
+ }
27266
+ }
27267
+ catch (error) {
27268
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27269
+ }
27270
+ }
27271
+ removeLoop() {
27272
+ if (!this.currentTable || !this.currentLoop)
27273
+ return;
27274
+ try {
27275
+ const success = this.currentTable.removeRowLoop(this.currentLoop.id);
27276
+ if (success) {
27277
+ this.hideLoop();
27278
+ this.onRemoveCallback?.(true);
27279
+ }
27280
+ else {
27281
+ this.onRemoveCallback?.(false);
27282
+ }
27283
+ }
27284
+ catch {
27285
+ this.onRemoveCallback?.(false);
27286
+ }
27287
+ }
27288
+ /**
27289
+ * Get the currently selected loop.
27290
+ */
27291
+ getCurrentLoop() {
27292
+ return this.currentLoop;
27293
+ }
27294
+ /**
27295
+ * Get the currently selected table.
27296
+ */
27297
+ getCurrentTable() {
27298
+ return this.currentTable;
27299
+ }
27300
+ /**
27301
+ * Check if a loop is currently selected.
27302
+ */
27303
+ hasLoop() {
27304
+ return this.currentLoop !== null;
27305
+ }
27306
+ /**
27307
+ * Update the pane from current editor state.
27308
+ */
27309
+ update() {
27310
+ // Table row loop pane doesn't auto-update - it's driven by showLoop() calls
27311
+ }
27312
+ }
27313
+
27314
+ /**
27315
+ * TextBoxPane - Edit text box properties.
27316
+ *
27317
+ * Shows:
27318
+ * - Position (inline, block, relative)
27319
+ * - Relative offset (for relative positioning)
27320
+ * - Background color
27321
+ * - Border (width, color, style)
27322
+ * - Padding
27323
+ *
27324
+ * Uses the PCEditor public API:
27325
+ * - editor.getSelectedTextBox()
27326
+ * - editor.updateTextBox()
27327
+ */
27328
+ class TextBoxPane extends BasePane {
27329
+ constructor(id = 'textbox', options = {}) {
27330
+ super(id, { className: 'pc-pane-textbox', ...options });
27331
+ this.positionSelect = null;
27332
+ this.offsetGroup = null;
27333
+ this.offsetXInput = null;
27334
+ this.offsetYInput = null;
27335
+ this.bgColorInput = null;
27336
+ this.borderWidthInput = null;
27337
+ this.borderColorInput = null;
27338
+ this.borderStyleSelect = null;
27339
+ this.paddingInput = null;
27340
+ this._isUpdating = false;
27341
+ this.currentTextBox = null;
27342
+ this.onApplyCallback = options.onApply;
27343
+ }
27344
+ attach(options) {
27345
+ super.attach(options);
27346
+ if (this.editor) {
27347
+ // Listen for selection changes
27348
+ const updateHandler = () => this.updateFromSelection();
27349
+ this.editor.on('selection-change', updateHandler);
27350
+ this.editor.on('textbox-updated', updateHandler);
27351
+ this.eventCleanup.push(() => {
27352
+ this.editor?.off('selection-change', updateHandler);
27353
+ this.editor?.off('textbox-updated', updateHandler);
27354
+ });
27355
+ // Initial update
27356
+ this.updateFromSelection();
27357
+ }
27358
+ }
27359
+ createContent() {
27360
+ const container = document.createElement('div');
27361
+ // Position section
27362
+ const positionSection = this.createSection('Position');
27363
+ this.positionSelect = this.createSelect([
27364
+ { value: 'inline', label: 'Inline' },
27365
+ { value: 'block', label: 'Block' },
27366
+ { value: 'relative', label: 'Relative' }
27367
+ ], 'inline');
27368
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27369
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27370
+ // Offset group (only visible for relative positioning)
27371
+ this.offsetGroup = document.createElement('div');
27372
+ this.offsetGroup.style.display = 'none';
27373
+ const offsetRow = this.createRow();
27374
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27375
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27376
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27377
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27378
+ this.offsetGroup.appendChild(offsetRow);
27379
+ positionSection.appendChild(this.offsetGroup);
27380
+ container.appendChild(positionSection);
27381
+ // Background section
27382
+ const bgSection = this.createSection('Background');
27383
+ this.bgColorInput = this.createColorInput('#ffffff');
27384
+ bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
27385
+ container.appendChild(bgSection);
27386
+ // Border section
27387
+ const borderSection = this.createSection('Border');
27388
+ const borderRow = this.createRow();
27389
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27390
+ this.borderColorInput = this.createColorInput('#cccccc');
27391
+ borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27392
+ borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27393
+ borderSection.appendChild(borderRow);
27394
+ this.borderStyleSelect = this.createSelect([
27395
+ { value: 'solid', label: 'Solid' },
27396
+ { value: 'dashed', label: 'Dashed' },
27397
+ { value: 'dotted', label: 'Dotted' },
27398
+ { value: 'none', label: 'None' }
27399
+ ], 'solid');
27400
+ borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27401
+ container.appendChild(borderSection);
27402
+ // Padding section
27403
+ const paddingSection = this.createSection('Padding');
27404
+ this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27405
+ paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
27406
+ container.appendChild(paddingSection);
27407
+ // Apply button
27408
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27409
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27410
+ container.appendChild(applyBtn);
27411
+ return container;
27412
+ }
27413
+ updateFromSelection() {
27414
+ if (!this.editor || this._isUpdating)
27415
+ return;
27416
+ this._isUpdating = true;
27417
+ try {
27418
+ const textBox = this.editor.getSelectedTextBox?.();
27419
+ if (textBox && !textBox.editing) {
27420
+ this.showTextBox(textBox);
27421
+ }
27422
+ else {
27423
+ this.hideTextBox();
27424
+ }
27425
+ }
27426
+ finally {
27427
+ this._isUpdating = false;
27428
+ }
27429
+ }
27430
+ /**
27431
+ * Show the pane with the given text box.
27432
+ */
27433
+ showTextBox(textBox) {
27434
+ this.currentTextBox = textBox;
27435
+ // Populate position
27436
+ if (this.positionSelect) {
27437
+ this.positionSelect.value = textBox.position || 'inline';
27438
+ }
27439
+ this.updateOffsetVisibility();
27440
+ // Populate offset
27441
+ if (this.offsetXInput) {
27442
+ this.offsetXInput.value = String(textBox.relativeOffset?.x ?? 0);
27443
+ }
27444
+ if (this.offsetYInput) {
27445
+ this.offsetYInput.value = String(textBox.relativeOffset?.y ?? 0);
27446
+ }
27447
+ // Populate background
27448
+ if (this.bgColorInput) {
27449
+ this.bgColorInput.value = textBox.backgroundColor || '#ffffff';
27450
+ }
27451
+ // Populate border (use first side with non-none style)
27452
+ const border = textBox.border;
27453
+ const activeBorder = border.top.style !== 'none' ? border.top :
27454
+ border.right.style !== 'none' ? border.right :
27455
+ border.bottom.style !== 'none' ? border.bottom :
27456
+ border.left.style !== 'none' ? border.left : border.top;
27457
+ if (this.borderWidthInput) {
27458
+ this.borderWidthInput.value = String(activeBorder.width);
27459
+ }
27460
+ if (this.borderColorInput) {
27461
+ this.borderColorInput.value = activeBorder.color;
27462
+ }
27463
+ if (this.borderStyleSelect) {
27464
+ this.borderStyleSelect.value = activeBorder.style;
27465
+ }
27466
+ // Populate padding
27467
+ if (this.paddingInput) {
27468
+ this.paddingInput.value = String(textBox.padding ?? 8);
27469
+ }
27470
+ this.show();
27471
+ }
27472
+ /**
27473
+ * Hide the pane and clear current text box.
27474
+ */
27475
+ hideTextBox() {
27476
+ this.currentTextBox = null;
27477
+ this.hide();
27478
+ }
27479
+ updateOffsetVisibility() {
27480
+ if (this.offsetGroup && this.positionSelect) {
27481
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27482
+ }
27483
+ }
27484
+ applyChanges() {
27485
+ if (!this.editor || !this.currentTextBox) {
27486
+ this.onApplyCallback?.(false, new Error('No text box selected'));
27487
+ return;
27488
+ }
27489
+ const updates = {};
27490
+ // Position
27491
+ if (this.positionSelect) {
27492
+ updates.position = this.positionSelect.value;
27493
+ }
27494
+ // Relative offset
27495
+ if (this.positionSelect?.value === 'relative') {
27496
+ updates.relativeOffset = {
27497
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27498
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27499
+ };
27500
+ }
27501
+ // Background color
27502
+ if (this.bgColorInput) {
27503
+ updates.backgroundColor = this.bgColorInput.value;
27504
+ }
27505
+ // Border
27506
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27507
+ const color = this.borderColorInput?.value || '#cccccc';
27508
+ const style = (this.borderStyleSelect?.value || 'solid');
27509
+ const borderSide = { width, color, style };
27510
+ updates.border = {
27511
+ top: { ...borderSide },
27512
+ right: { ...borderSide },
27513
+ bottom: { ...borderSide },
27514
+ left: { ...borderSide }
27515
+ };
27516
+ // Padding
27517
+ if (this.paddingInput) {
27518
+ updates.padding = parseInt(this.paddingInput.value, 10);
27519
+ }
27520
+ try {
27521
+ const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27522
+ if (success) {
27523
+ this.onApplyCallback?.(true);
27524
+ }
27525
+ else {
27526
+ this.onApplyCallback?.(false, new Error('Failed to update text box'));
27527
+ }
27528
+ }
27529
+ catch (error) {
27530
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27531
+ }
27532
+ }
27533
+ /**
27534
+ * Get the currently selected text box.
27535
+ */
27536
+ getCurrentTextBox() {
27537
+ return this.currentTextBox;
27538
+ }
27539
+ /**
27540
+ * Check if a text box is currently selected.
27541
+ */
27542
+ hasTextBox() {
27543
+ return this.currentTextBox !== null;
27544
+ }
27545
+ /**
27546
+ * Update the pane from current editor state.
27547
+ */
27548
+ update() {
27549
+ this.updateFromSelection();
27550
+ }
27551
+ }
27552
+
27553
+ /**
27554
+ * ImagePane - Edit image properties.
27555
+ *
27556
+ * Shows:
27557
+ * - Position (inline, block, relative)
27558
+ * - Relative offset (for relative positioning)
27559
+ * - Fit mode (contain, cover, fill, none, tile)
27560
+ * - Resize mode (free, locked-aspect-ratio)
27561
+ * - Alt text
27562
+ * - Source file picker
27563
+ *
27564
+ * Uses the PCEditor public API:
27565
+ * - editor.getSelectedImage()
27566
+ * - editor.updateImage()
27567
+ * - editor.setImageSource()
27568
+ */
27569
+ class ImagePane extends BasePane {
27570
+ constructor(id = 'image', options = {}) {
27571
+ super(id, { className: 'pc-pane-image', ...options });
27572
+ this.positionSelect = null;
27573
+ this.offsetGroup = null;
27574
+ this.offsetXInput = null;
27575
+ this.offsetYInput = null;
27576
+ this.fitModeSelect = null;
27577
+ this.resizeModeSelect = null;
27578
+ this.altTextInput = null;
27579
+ this.fileInput = null;
27580
+ this.currentImage = null;
27581
+ this._isUpdating = false;
27582
+ this.maxImageWidth = options.maxImageWidth ?? 400;
27583
+ this.maxImageHeight = options.maxImageHeight ?? 400;
27584
+ this.onApplyCallback = options.onApply;
27585
+ }
27586
+ attach(options) {
27587
+ super.attach(options);
27588
+ if (this.editor) {
27589
+ // Listen for selection changes
27590
+ const updateHandler = () => this.updateFromSelection();
27591
+ this.editor.on('selection-change', updateHandler);
27592
+ this.editor.on('image-updated', updateHandler);
27593
+ this.eventCleanup.push(() => {
27594
+ this.editor?.off('selection-change', updateHandler);
27595
+ this.editor?.off('image-updated', updateHandler);
27596
+ });
27597
+ // Initial update
27598
+ this.updateFromSelection();
27599
+ }
27600
+ }
27601
+ createContent() {
27602
+ const container = document.createElement('div');
27603
+ // Position section
27604
+ const positionSection = this.createSection('Position');
27605
+ this.positionSelect = this.createSelect([
27606
+ { value: 'inline', label: 'Inline' },
27607
+ { value: 'block', label: 'Block' },
27608
+ { value: 'relative', label: 'Relative' }
27609
+ ], 'inline');
27610
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27611
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27612
+ // Offset group (only visible for relative positioning)
27613
+ this.offsetGroup = document.createElement('div');
27614
+ this.offsetGroup.style.display = 'none';
27615
+ const offsetRow = this.createRow();
27616
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27617
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27618
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27619
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27620
+ this.offsetGroup.appendChild(offsetRow);
27621
+ positionSection.appendChild(this.offsetGroup);
27622
+ container.appendChild(positionSection);
27623
+ // Fit mode section
27624
+ const fitSection = this.createSection('Display');
27625
+ this.fitModeSelect = this.createSelect([
27626
+ { value: 'contain', label: 'Contain' },
27627
+ { value: 'cover', label: 'Cover' },
27628
+ { value: 'fill', label: 'Fill' },
27629
+ { value: 'none', label: 'None (original size)' },
27630
+ { value: 'tile', label: 'Tile' }
27631
+ ], 'contain');
27632
+ fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
27633
+ this.resizeModeSelect = this.createSelect([
27634
+ { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27635
+ { value: 'free', label: 'Free Resize' }
27636
+ ], 'locked-aspect-ratio');
27637
+ fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27638
+ container.appendChild(fitSection);
27639
+ // Alt text section
27640
+ const altSection = this.createSection('Accessibility');
27641
+ this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27642
+ altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27643
+ container.appendChild(altSection);
27644
+ // Source section
27645
+ const sourceSection = this.createSection('Source');
27646
+ this.fileInput = document.createElement('input');
27647
+ this.fileInput.type = 'file';
27648
+ this.fileInput.accept = 'image/*';
27649
+ this.fileInput.style.display = 'none';
27650
+ this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27651
+ sourceSection.appendChild(this.fileInput);
27652
+ const changeSourceBtn = this.createButton('Change Image...');
27653
+ this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27654
+ sourceSection.appendChild(changeSourceBtn);
27655
+ container.appendChild(sourceSection);
27656
+ // Apply button
27657
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27658
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27659
+ container.appendChild(applyBtn);
27660
+ return container;
27661
+ }
27662
+ updateFromSelection() {
27663
+ if (!this.editor || this._isUpdating)
27664
+ return;
27665
+ this._isUpdating = true;
27666
+ try {
27667
+ const image = this.editor.getSelectedImage?.();
27668
+ if (image) {
27669
+ this.showImage(image);
27670
+ }
27671
+ else {
27672
+ this.hideImage();
27673
+ }
27674
+ }
27675
+ finally {
27676
+ this._isUpdating = false;
27677
+ }
27678
+ }
27679
+ /**
27680
+ * Show the pane with the given image.
27681
+ */
27682
+ showImage(image) {
27683
+ this.currentImage = image;
27684
+ // Populate position
27685
+ if (this.positionSelect) {
27686
+ this.positionSelect.value = image.position || 'inline';
27687
+ }
27688
+ this.updateOffsetVisibility();
27689
+ // Populate offset
27690
+ if (this.offsetXInput) {
27691
+ this.offsetXInput.value = String(image.relativeOffset?.x ?? 0);
27692
+ }
27693
+ if (this.offsetYInput) {
27694
+ this.offsetYInput.value = String(image.relativeOffset?.y ?? 0);
27695
+ }
27696
+ // Populate fit mode
27697
+ if (this.fitModeSelect) {
27698
+ this.fitModeSelect.value = image.fit || 'contain';
27699
+ }
27700
+ // Populate resize mode
27701
+ if (this.resizeModeSelect) {
27702
+ this.resizeModeSelect.value = image.resizeMode || 'locked-aspect-ratio';
27703
+ }
27704
+ // Populate alt text
27705
+ if (this.altTextInput) {
27706
+ this.altTextInput.value = image.alt || '';
27707
+ }
27708
+ this.show();
27709
+ }
27710
+ /**
27711
+ * Hide the pane and clear current image.
27712
+ */
27713
+ hideImage() {
27714
+ this.currentImage = null;
27715
+ this.hide();
27716
+ }
27717
+ updateOffsetVisibility() {
27718
+ if (this.offsetGroup && this.positionSelect) {
27719
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27720
+ }
27721
+ }
27722
+ handleFileChange(event) {
27723
+ if (!this.editor || !this.currentImage)
27724
+ return;
27725
+ const input = event.target;
27726
+ const file = input.files?.[0];
27727
+ if (!file)
27728
+ return;
27729
+ const reader = new FileReader();
27730
+ reader.onload = (e) => {
27731
+ const dataUrl = e.target?.result;
27732
+ if (dataUrl && this.currentImage && this.editor) {
27733
+ this.editor.setImageSource(this.currentImage.id, dataUrl, {
27734
+ maxWidth: this.maxImageWidth,
27735
+ maxHeight: this.maxImageHeight
27736
+ });
27737
+ }
27738
+ };
27739
+ reader.readAsDataURL(file);
27740
+ // Reset file input so the same file can be selected again
27741
+ input.value = '';
27742
+ }
27743
+ applyChanges() {
27744
+ if (!this.editor || !this.currentImage) {
27745
+ this.onApplyCallback?.(false, new Error('No image selected'));
27746
+ return;
27747
+ }
27748
+ const updates = {};
27749
+ // Position
27750
+ if (this.positionSelect) {
27751
+ updates.position = this.positionSelect.value;
27752
+ }
27753
+ // Relative offset
27754
+ if (this.positionSelect?.value === 'relative') {
27755
+ updates.relativeOffset = {
27756
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27757
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27758
+ };
27759
+ }
27760
+ // Fit mode
27761
+ if (this.fitModeSelect) {
27762
+ updates.fit = this.fitModeSelect.value;
27763
+ }
27764
+ // Resize mode
27765
+ if (this.resizeModeSelect) {
27766
+ updates.resizeMode = this.resizeModeSelect.value;
27767
+ }
27768
+ // Alt text
27769
+ if (this.altTextInput) {
27770
+ updates.alt = this.altTextInput.value;
27771
+ }
27772
+ try {
27773
+ const success = this.editor.updateImage(this.currentImage.id, updates);
27774
+ if (success) {
27775
+ this.onApplyCallback?.(true);
27776
+ }
27777
+ else {
27778
+ this.onApplyCallback?.(false, new Error('Failed to update image'));
27779
+ }
27780
+ }
27781
+ catch (error) {
27782
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27783
+ }
27784
+ }
27785
+ /**
27786
+ * Get the currently selected image.
27787
+ */
27788
+ getCurrentImage() {
27789
+ return this.currentImage;
27790
+ }
27791
+ /**
27792
+ * Check if an image is currently selected.
27793
+ */
27794
+ hasImage() {
27795
+ return this.currentImage !== null;
27796
+ }
27797
+ /**
27798
+ * Update the pane from current editor state.
27799
+ */
27800
+ update() {
27801
+ this.updateFromSelection();
27802
+ }
27803
+ }
27804
+
27805
+ /**
27806
+ * TablePane - Edit table properties.
27807
+ *
27808
+ * Shows:
27809
+ * - Table structure (row/column count)
27810
+ * - Row/column insertion/removal
27811
+ * - Header rows/columns
27812
+ * - Default cell padding and border color
27813
+ * - Cell-specific formatting (background, borders)
27814
+ *
27815
+ * Uses the PCEditor public API:
27816
+ * - editor.getFocusedTable()
27817
+ * - editor.tableInsertRow()
27818
+ * - editor.tableRemoveRow()
27819
+ * - editor.tableInsertColumn()
27820
+ * - editor.tableRemoveColumn()
27821
+ *
27822
+ * And TableObject methods:
27823
+ * - table.setHeaderRowCount()
27824
+ * - table.setHeaderColumnCount()
27825
+ * - table.getCell()
27826
+ * - table.getCellsInRange()
27827
+ */
27828
+ class TablePane extends BasePane {
27829
+ constructor(id = 'table', options = {}) {
27830
+ super(id, { className: 'pc-pane-table', ...options });
27831
+ // Structure info
27832
+ this.rowCountDisplay = null;
27833
+ this.colCountDisplay = null;
27834
+ this.cellSelectionDisplay = null;
27835
+ // Header controls
27836
+ this.headerRowInput = null;
27837
+ this.headerColInput = null;
27838
+ // Default controls
27839
+ this.defaultPaddingInput = null;
27840
+ this.defaultBorderColorInput = null;
27841
+ // Row loop controls
27842
+ this.loopFieldInput = null;
27843
+ // Cell formatting controls
27844
+ this.cellBgColorInput = null;
27845
+ this.borderTopCheck = null;
27846
+ this.borderRightCheck = null;
27847
+ this.borderBottomCheck = null;
27848
+ this.borderLeftCheck = null;
27849
+ this.borderWidthInput = null;
27850
+ this.borderColorInput = null;
27851
+ this.borderStyleSelect = null;
27852
+ this.currentTable = null;
27853
+ this._isUpdating = false;
27854
+ this.onApplyCallback = options.onApply;
27855
+ }
27856
+ attach(options) {
27857
+ super.attach(options);
27858
+ if (this.editor) {
27859
+ // Listen for selection/focus changes
27860
+ const updateHandler = () => this.updateFromFocusedTable();
27861
+ this.editor.on('selection-change', updateHandler);
27862
+ this.editor.on('table-cell-focus', updateHandler);
27863
+ this.editor.on('table-cell-selection', updateHandler);
27864
+ this.eventCleanup.push(() => {
27865
+ this.editor?.off('selection-change', updateHandler);
27866
+ this.editor?.off('table-cell-focus', updateHandler);
27867
+ this.editor?.off('table-cell-selection', updateHandler);
27868
+ });
27869
+ // Initial update
27870
+ this.updateFromFocusedTable();
27871
+ }
27872
+ }
27873
+ createContent() {
27874
+ const container = document.createElement('div');
27875
+ // Structure section
27876
+ const structureSection = this.createSection('Structure');
27877
+ const structureInfo = document.createElement('div');
27878
+ structureInfo.className = 'pc-pane-info-list';
27879
+ this.rowCountDisplay = document.createElement('span');
27880
+ this.colCountDisplay = document.createElement('span');
27881
+ const rowInfo = document.createElement('div');
27882
+ rowInfo.className = 'pc-pane-info';
27883
+ rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27884
+ rowInfo.appendChild(this.rowCountDisplay);
27885
+ const colInfo = document.createElement('div');
27886
+ colInfo.className = 'pc-pane-info';
27887
+ colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27888
+ colInfo.appendChild(this.colCountDisplay);
27889
+ structureInfo.appendChild(rowInfo);
27890
+ structureInfo.appendChild(colInfo);
27891
+ structureSection.appendChild(structureInfo);
27892
+ // Row/column buttons
27893
+ const structureBtns = this.createButtonGroup();
27894
+ const addRowBtn = this.createButton('+ Row');
27895
+ this.addButtonListener(addRowBtn, () => this.insertRow());
27896
+ const removeRowBtn = this.createButton('- Row');
27897
+ this.addButtonListener(removeRowBtn, () => this.removeRow());
27898
+ const addColBtn = this.createButton('+ Column');
27899
+ this.addButtonListener(addColBtn, () => this.insertColumn());
27900
+ const removeColBtn = this.createButton('- Column');
27901
+ this.addButtonListener(removeColBtn, () => this.removeColumn());
27902
+ structureBtns.appendChild(addRowBtn);
27903
+ structureBtns.appendChild(removeRowBtn);
27904
+ structureBtns.appendChild(addColBtn);
27905
+ structureBtns.appendChild(removeColBtn);
27906
+ structureSection.appendChild(structureBtns);
27907
+ container.appendChild(structureSection);
27908
+ // Headers section
27909
+ const headersSection = this.createSection('Headers');
27910
+ const headerRow = this.createRow();
27911
+ this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27912
+ this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27913
+ headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27914
+ headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27915
+ headersSection.appendChild(headerRow);
27916
+ const applyHeadersBtn = this.createButton('Apply Headers');
27917
+ this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27918
+ headersSection.appendChild(applyHeadersBtn);
27919
+ container.appendChild(headersSection);
27920
+ // Row Loop section
27921
+ const loopSection = this.createSection('Row Loop');
27922
+ this.loopFieldInput = this.createTextInput({ placeholder: 'items' });
27923
+ loopSection.appendChild(this.createFormGroup('Array Field', this.loopFieldInput, {
27924
+ hint: 'Creates a loop on the currently focused row'
27925
+ }));
27926
+ const createLoopBtn = this.createButton('Create Row Loop');
27927
+ this.addButtonListener(createLoopBtn, () => this.createRowLoop());
27928
+ loopSection.appendChild(createLoopBtn);
27929
+ container.appendChild(loopSection);
27930
+ // Defaults section
27931
+ const defaultsSection = this.createSection('Defaults');
27932
+ const defaultsRow = this.createRow();
27933
+ this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
27934
+ this.defaultBorderColorInput = this.createColorInput('#cccccc');
27935
+ defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27936
+ defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27937
+ defaultsSection.appendChild(defaultsRow);
27938
+ const applyDefaultsBtn = this.createButton('Apply Defaults');
27939
+ this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27940
+ defaultsSection.appendChild(applyDefaultsBtn);
27941
+ container.appendChild(defaultsSection);
27942
+ // Cell formatting section
27943
+ const cellSection = this.createSection('Cell Formatting');
27944
+ this.cellSelectionDisplay = this.createHint('No cell selected');
27945
+ cellSection.appendChild(this.cellSelectionDisplay);
27946
+ // Background
27947
+ this.cellBgColorInput = this.createColorInput('#ffffff');
27948
+ cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
27949
+ // Border checkboxes
27950
+ const borderChecks = document.createElement('div');
27951
+ borderChecks.className = 'pc-pane-row';
27952
+ borderChecks.style.flexWrap = 'wrap';
27953
+ borderChecks.style.gap = '4px';
27954
+ this.borderTopCheck = document.createElement('input');
27955
+ this.borderTopCheck.type = 'checkbox';
27956
+ this.borderTopCheck.checked = true;
27957
+ this.borderRightCheck = document.createElement('input');
27958
+ this.borderRightCheck.type = 'checkbox';
27959
+ this.borderRightCheck.checked = true;
27960
+ this.borderBottomCheck = document.createElement('input');
27961
+ this.borderBottomCheck.type = 'checkbox';
27962
+ this.borderBottomCheck.checked = true;
27963
+ this.borderLeftCheck = document.createElement('input');
27964
+ this.borderLeftCheck.type = 'checkbox';
27965
+ this.borderLeftCheck.checked = true;
27966
+ borderChecks.appendChild(this.createCheckbox('Top', true));
27967
+ borderChecks.appendChild(this.createCheckbox('Right', true));
27968
+ borderChecks.appendChild(this.createCheckbox('Bottom', true));
27969
+ borderChecks.appendChild(this.createCheckbox('Left', true));
27970
+ // Replace created checkboxes with our tracked ones
27971
+ const checkLabels = borderChecks.querySelectorAll('label');
27972
+ if (checkLabels[0])
27973
+ checkLabels[0].replaceChild(this.borderTopCheck, checkLabels[0].querySelector('input'));
27974
+ if (checkLabels[1])
27975
+ checkLabels[1].replaceChild(this.borderRightCheck, checkLabels[1].querySelector('input'));
27976
+ if (checkLabels[2])
27977
+ checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
27978
+ if (checkLabels[3])
27979
+ checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
27980
+ cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
27981
+ // Border properties
27982
+ const borderPropsRow = this.createRow();
27983
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
27984
+ this.borderColorInput = this.createColorInput('#cccccc');
27985
+ borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27986
+ borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27987
+ cellSection.appendChild(borderPropsRow);
27988
+ this.borderStyleSelect = this.createSelect([
27989
+ { value: 'solid', label: 'Solid' },
27990
+ { value: 'dashed', label: 'Dashed' },
27991
+ { value: 'dotted', label: 'Dotted' },
27992
+ { value: 'none', label: 'None' }
27993
+ ], 'solid');
27994
+ cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27995
+ const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
27996
+ this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
27997
+ cellSection.appendChild(applyCellBtn);
27998
+ container.appendChild(cellSection);
27999
+ return container;
28000
+ }
28001
+ updateFromFocusedTable() {
28002
+ if (!this.editor || this._isUpdating)
28003
+ return;
28004
+ this._isUpdating = true;
28005
+ try {
28006
+ const table = this.editor.getFocusedTable();
28007
+ if (table) {
28008
+ this.showTable(table);
28009
+ }
28010
+ else {
28011
+ this.hideTable();
28012
+ }
28013
+ }
28014
+ finally {
28015
+ this._isUpdating = false;
28016
+ }
28017
+ }
28018
+ /**
28019
+ * Show the pane with the given table.
28020
+ */
28021
+ showTable(table) {
28022
+ this.currentTable = table;
28023
+ // Update structure info
28024
+ if (this.rowCountDisplay) {
28025
+ this.rowCountDisplay.textContent = String(table.rowCount);
28026
+ this.rowCountDisplay.className = 'pc-pane-info-value';
28027
+ }
28028
+ if (this.colCountDisplay) {
28029
+ this.colCountDisplay.textContent = String(table.columnCount);
28030
+ this.colCountDisplay.className = 'pc-pane-info-value';
28031
+ }
28032
+ // Update header counts
28033
+ if (this.headerRowInput) {
28034
+ this.headerRowInput.value = String(table.headerRowCount);
28035
+ }
28036
+ if (this.headerColInput) {
28037
+ this.headerColInput.value = String(table.headerColumnCount);
28038
+ }
28039
+ // Update defaults
28040
+ if (this.defaultPaddingInput) {
28041
+ this.defaultPaddingInput.value = String(table.defaultCellPadding);
28042
+ }
28043
+ if (this.defaultBorderColorInput) {
28044
+ this.defaultBorderColorInput.value = table.defaultBorderColor;
28045
+ }
28046
+ // Update cell selection info
28047
+ this.updateCellSelectionInfo(table);
28048
+ this.show();
28049
+ }
28050
+ /**
28051
+ * Hide the pane and clear current table.
28052
+ */
28053
+ hideTable() {
28054
+ this.currentTable = null;
28055
+ this.hide();
28056
+ }
28057
+ updateCellSelectionInfo(table) {
28058
+ if (!this.cellSelectionDisplay)
28059
+ return;
28060
+ const focusedCell = table.focusedCell;
28061
+ const selectedRange = table.selectedRange;
28062
+ if (selectedRange) {
28063
+ const count = (selectedRange.end.row - selectedRange.start.row + 1) *
28064
+ (selectedRange.end.col - selectedRange.start.col + 1);
28065
+ this.cellSelectionDisplay.textContent = `${count} cells selected`;
28066
+ }
28067
+ else if (focusedCell) {
28068
+ this.cellSelectionDisplay.textContent = `Cell [${focusedCell.row}, ${focusedCell.col}]`;
28069
+ // Update cell formatting controls from focused cell
28070
+ const cell = table.getCell(focusedCell.row, focusedCell.col);
28071
+ if (cell) {
28072
+ if (this.cellBgColorInput) {
28073
+ this.cellBgColorInput.value = cell.backgroundColor || '#ffffff';
28074
+ }
28075
+ // Update border controls
28076
+ const border = cell.border;
28077
+ if (this.borderTopCheck)
28078
+ this.borderTopCheck.checked = border.top.style !== 'none';
28079
+ if (this.borderRightCheck)
28080
+ this.borderRightCheck.checked = border.right.style !== 'none';
28081
+ if (this.borderBottomCheck)
28082
+ this.borderBottomCheck.checked = border.bottom.style !== 'none';
28083
+ if (this.borderLeftCheck)
28084
+ this.borderLeftCheck.checked = border.left.style !== 'none';
28085
+ // Use first active border for properties
28086
+ const activeBorder = border.top.style !== 'none' ? border.top :
28087
+ border.right.style !== 'none' ? border.right :
28088
+ border.bottom.style !== 'none' ? border.bottom :
28089
+ border.left.style !== 'none' ? border.left : border.top;
28090
+ if (this.borderWidthInput)
28091
+ this.borderWidthInput.value = String(activeBorder.width);
28092
+ if (this.borderColorInput)
28093
+ this.borderColorInput.value = activeBorder.color;
28094
+ if (this.borderStyleSelect)
28095
+ this.borderStyleSelect.value = activeBorder.style;
28096
+ }
28097
+ }
28098
+ else {
28099
+ this.cellSelectionDisplay.textContent = 'No cell selected';
28100
+ }
28101
+ }
28102
+ insertRow() {
28103
+ if (!this.editor || !this.currentTable)
28104
+ return;
28105
+ const focusedCell = this.currentTable.focusedCell;
28106
+ const rowIndex = focusedCell ? focusedCell.row + 1 : this.currentTable.rowCount;
28107
+ this.editor.tableInsertRow(this.currentTable, rowIndex);
28108
+ this.updateFromFocusedTable();
28109
+ }
28110
+ removeRow() {
28111
+ if (!this.editor || !this.currentTable)
28112
+ return;
28113
+ const focusedCell = this.currentTable.focusedCell;
28114
+ if (focusedCell && this.currentTable.rowCount > 1) {
28115
+ this.editor.tableRemoveRow(this.currentTable, focusedCell.row);
28116
+ this.updateFromFocusedTable();
28117
+ }
28118
+ }
28119
+ insertColumn() {
28120
+ if (!this.editor || !this.currentTable)
28121
+ return;
28122
+ const focusedCell = this.currentTable.focusedCell;
28123
+ const colIndex = focusedCell ? focusedCell.col + 1 : this.currentTable.columnCount;
28124
+ this.editor.tableInsertColumn(this.currentTable, colIndex);
28125
+ this.updateFromFocusedTable();
28126
+ }
28127
+ removeColumn() {
28128
+ if (!this.editor || !this.currentTable)
28129
+ return;
28130
+ const focusedCell = this.currentTable.focusedCell;
28131
+ if (focusedCell && this.currentTable.columnCount > 1) {
28132
+ this.editor.tableRemoveColumn(this.currentTable, focusedCell.col);
28133
+ this.updateFromFocusedTable();
28134
+ }
28135
+ }
28136
+ applyHeaders() {
28137
+ if (!this.currentTable)
28138
+ return;
28139
+ if (this.headerRowInput) {
28140
+ const count = parseInt(this.headerRowInput.value, 10);
28141
+ this.currentTable.setHeaderRowCount(count);
28142
+ }
28143
+ if (this.headerColInput) {
28144
+ const count = parseInt(this.headerColInput.value, 10);
28145
+ this.currentTable.setHeaderColumnCount(count);
28146
+ }
28147
+ this.editor?.render();
28148
+ this.onApplyCallback?.(true);
28149
+ }
28150
+ applyDefaults() {
28151
+ if (!this.currentTable)
28152
+ return;
28153
+ if (this.defaultPaddingInput) {
28154
+ this.currentTable.defaultCellPadding = parseInt(this.defaultPaddingInput.value, 10);
28155
+ }
28156
+ if (this.defaultBorderColorInput) {
28157
+ this.currentTable.defaultBorderColor = this.defaultBorderColorInput.value;
28158
+ }
28159
+ this.editor?.render();
28160
+ this.onApplyCallback?.(true);
28161
+ }
28162
+ applyCellFormatting() {
28163
+ if (!this.currentTable)
28164
+ return;
28165
+ const focusedCell = this.currentTable.focusedCell;
28166
+ const selectedRange = this.currentTable.selectedRange;
28167
+ // Determine cells to update
28168
+ const cells = [];
28169
+ if (selectedRange) {
28170
+ for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
28171
+ for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
28172
+ cells.push({ row, col });
28173
+ }
28174
+ }
28175
+ }
28176
+ else if (focusedCell) {
28177
+ cells.push(focusedCell);
28178
+ }
28179
+ if (cells.length === 0)
28180
+ return;
28181
+ // Build border config
28182
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
28183
+ const color = this.borderColorInput?.value || '#cccccc';
28184
+ const style = (this.borderStyleSelect?.value || 'solid');
28185
+ const borderSide = { width, color, style };
28186
+ const noneBorder = { width: 0, color: '#000000', style: 'none' };
28187
+ const border = {
28188
+ top: this.borderTopCheck?.checked ? { ...borderSide } : { ...noneBorder },
28189
+ right: this.borderRightCheck?.checked ? { ...borderSide } : { ...noneBorder },
28190
+ bottom: this.borderBottomCheck?.checked ? { ...borderSide } : { ...noneBorder },
28191
+ left: this.borderLeftCheck?.checked ? { ...borderSide } : { ...noneBorder }
28192
+ };
28193
+ const bgColor = this.cellBgColorInput?.value;
28194
+ // Apply to each cell
28195
+ for (const { row, col } of cells) {
28196
+ const cell = this.currentTable.getCell(row, col);
28197
+ if (cell) {
28198
+ if (bgColor) {
28199
+ cell.backgroundColor = bgColor;
28200
+ }
28201
+ cell.border = border;
28202
+ }
28203
+ }
28204
+ this.editor?.render();
28205
+ this.onApplyCallback?.(true);
28206
+ }
28207
+ /**
28208
+ * Get the currently focused table.
28209
+ */
28210
+ getCurrentTable() {
28211
+ return this.currentTable;
28212
+ }
28213
+ /**
28214
+ * Check if a table is currently focused.
28215
+ */
28216
+ hasTable() {
28217
+ return this.currentTable !== null;
28218
+ }
28219
+ createRowLoop() {
28220
+ if (!this.editor || !this.currentTable) {
28221
+ this.onApplyCallback?.(false, new Error('No table focused'));
28222
+ return;
28223
+ }
28224
+ const fieldPath = this.loopFieldInput?.value.trim() || '';
28225
+ if (!fieldPath) {
28226
+ this.onApplyCallback?.(false, new Error('Array field path is required'));
28227
+ return;
28228
+ }
28229
+ // Uses the unified createRepeatingSection API which detects
28230
+ // that a table is focused and creates a row loop on the focused row
28231
+ this.editor.createRepeatingSection(0, 0, fieldPath);
28232
+ this.onApplyCallback?.(true);
28233
+ }
28234
+ /**
28235
+ * Update the pane from current editor state.
28236
+ */
28237
+ update() {
28238
+ this.updateFromFocusedTable();
28239
+ }
28240
+ }
28241
+
25182
28242
  exports.BaseControl = BaseControl;
25183
28243
  exports.BaseEmbeddedObject = BaseEmbeddedObject;
28244
+ exports.BasePane = BasePane;
25184
28245
  exports.BaseTextRegion = BaseTextRegion;
25185
28246
  exports.BodyTextRegion = BodyTextRegion;
25186
28247
  exports.ClipboardManager = ClipboardManager;
@@ -25188,15 +28249,22 @@ exports.ContentAnalyzer = ContentAnalyzer;
25188
28249
  exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
25189
28250
  exports.Document = Document;
25190
28251
  exports.DocumentBuilder = DocumentBuilder;
28252
+ exports.DocumentInfoPane = DocumentInfoPane;
28253
+ exports.DocumentSettingsPane = DocumentSettingsPane;
25191
28254
  exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
25192
28255
  exports.EmbeddedObjectManager = EmbeddedObjectManager;
25193
28256
  exports.EventEmitter = EventEmitter;
25194
28257
  exports.FlowingTextContent = FlowingTextContent;
25195
28258
  exports.FooterTextRegion = FooterTextRegion;
28259
+ exports.FormattingPane = FormattingPane;
25196
28260
  exports.HeaderTextRegion = HeaderTextRegion;
25197
28261
  exports.HorizontalRuler = HorizontalRuler;
25198
28262
  exports.HtmlConverter = HtmlConverter;
28263
+ exports.HyperlinkPane = HyperlinkPane;
25199
28264
  exports.ImageObject = ImageObject;
28265
+ exports.ImagePane = ImagePane;
28266
+ exports.Logger = Logger;
28267
+ exports.MergeDataPane = MergeDataPane;
25200
28268
  exports.PCEditor = PCEditor;
25201
28269
  exports.PDFImportError = PDFImportError;
25202
28270
  exports.PDFImporter = PDFImporter;
@@ -25204,16 +28272,22 @@ exports.PDFParser = PDFParser;
25204
28272
  exports.Page = Page;
25205
28273
  exports.RegionManager = RegionManager;
25206
28274
  exports.RepeatingSectionManager = RepeatingSectionManager;
28275
+ exports.RepeatingSectionPane = RepeatingSectionPane;
25207
28276
  exports.RulerControl = RulerControl;
25208
28277
  exports.SubstitutionFieldManager = SubstitutionFieldManager;
28278
+ exports.SubstitutionFieldPane = SubstitutionFieldPane;
25209
28279
  exports.TableCell = TableCell;
25210
28280
  exports.TableObject = TableObject;
28281
+ exports.TablePane = TablePane;
25211
28282
  exports.TableRow = TableRow;
28283
+ exports.TableRowLoopPane = TableRowLoopPane;
25212
28284
  exports.TextBoxObject = TextBoxObject;
28285
+ exports.TextBoxPane = TextBoxPane;
25213
28286
  exports.TextFormattingManager = TextFormattingManager;
25214
28287
  exports.TextLayout = TextLayout;
25215
28288
  exports.TextMeasurer = TextMeasurer;
25216
28289
  exports.TextPositionCalculator = TextPositionCalculator;
25217
28290
  exports.TextState = TextState;
25218
28291
  exports.VerticalRuler = VerticalRuler;
28292
+ exports.ViewSettingsPane = ViewSettingsPane;
25219
28293
  //# sourceMappingURL=pc-editor.js.map