@productcloudos/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/dist/pc-editor.esm.js +2777 -5
  3. package/dist/pc-editor.esm.js.map +1 -1
  4. package/dist/pc-editor.js +2789 -4
  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 +69 -0
  9. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  10. package/dist/types/lib/index.d.ts +2 -0
  11. package/dist/types/lib/index.d.ts.map +1 -1
  12. package/dist/types/lib/panes/BasePane.d.ts +117 -0
  13. package/dist/types/lib/panes/BasePane.d.ts.map +1 -0
  14. package/dist/types/lib/panes/DocumentInfoPane.d.ts +24 -0
  15. package/dist/types/lib/panes/DocumentInfoPane.d.ts.map +1 -0
  16. package/dist/types/lib/panes/DocumentSettingsPane.d.ts +28 -0
  17. package/dist/types/lib/panes/DocumentSettingsPane.d.ts.map +1 -0
  18. package/dist/types/lib/panes/FormattingPane.d.ts +82 -0
  19. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -0
  20. package/dist/types/lib/panes/HyperlinkPane.d.ts +66 -0
  21. package/dist/types/lib/panes/HyperlinkPane.d.ts.map +1 -0
  22. package/dist/types/lib/panes/ImagePane.d.ts +79 -0
  23. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -0
  24. package/dist/types/lib/panes/MergeDataPane.d.ts +55 -0
  25. package/dist/types/lib/panes/MergeDataPane.d.ts.map +1 -0
  26. package/dist/types/lib/panes/RepeatingSectionPane.d.ts +62 -0
  27. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -0
  28. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts +65 -0
  29. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts.map +1 -0
  30. package/dist/types/lib/panes/TablePane.d.ts +88 -0
  31. package/dist/types/lib/panes/TablePane.d.ts.map +1 -0
  32. package/dist/types/lib/panes/TableRowLoopPane.d.ts +68 -0
  33. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -0
  34. package/dist/types/lib/panes/TextBoxPane.d.ts +67 -0
  35. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -0
  36. package/dist/types/lib/panes/ViewSettingsPane.d.ts +52 -0
  37. package/dist/types/lib/panes/ViewSettingsPane.d.ts.map +1 -0
  38. package/dist/types/lib/panes/index.d.ts +34 -0
  39. package/dist/types/lib/panes/index.d.ts.map +1 -0
  40. package/dist/types/lib/panes/types.d.ts +111 -0
  41. package/dist/types/lib/panes/types.d.ts.map +1 -0
  42. package/dist/types/lib/text/TextFormatting.d.ts +9 -0
  43. package/dist/types/lib/text/TextFormatting.d.ts.map +1 -1
  44. package/dist/types/lib/undo/transaction/MutationUndo.d.ts.map +1 -1
  45. package/package.json +1 -1
@@ -843,6 +843,20 @@ class TextFormattingManager extends EventEmitter {
843
843
  this.emit('formatting-changed', { start, end, formatting });
844
844
  }
845
845
  }
846
+ /**
847
+ * Set formatting at a specific position, replacing all existing formatting.
848
+ * Unlike applyFormatting which merges, this replaces completely.
849
+ * Used by undo operations to restore exact previous state.
850
+ * @param position Character position
851
+ * @param formatting Complete formatting to set
852
+ * @param silent If true, don't emit the formatting-changed event
853
+ */
854
+ setFormattingAt(position, formatting, silent = false) {
855
+ this.formatting.set(position, { ...formatting });
856
+ if (!silent) {
857
+ this.emit('formatting-changed', { start: position, end: position + 1, formatting });
858
+ }
859
+ }
846
860
  /**
847
861
  * Remove formatting from a range, reverting to default.
848
862
  */
@@ -18839,11 +18853,11 @@ class MutationUndo {
18839
18853
  const data = mutation.data;
18840
18854
  // Restore deleted text
18841
18855
  content.insertTextAt(data.position, data.deletedText);
18842
- // Restore formatting
18856
+ // Restore formatting using setFormattingAt to replace completely
18843
18857
  if (data.deletedFormatting) {
18844
18858
  const fm = content.getFormattingManager();
18845
18859
  data.deletedFormatting.forEach((style, offset) => {
18846
- fm.applyFormatting(data.position + offset, data.position + offset + 1, style);
18860
+ fm.setFormattingAt(data.position + offset, style, true);
18847
18861
  });
18848
18862
  }
18849
18863
  // Restore substitution fields
@@ -18869,9 +18883,10 @@ class MutationUndo {
18869
18883
  return;
18870
18884
  const data = mutation.data;
18871
18885
  const fm = content.getFormattingManager();
18872
- // Restore previous formatting
18886
+ // Restore previous formatting using setFormattingAt to replace completely
18887
+ // (not merge, which would leave properties like backgroundColor intact)
18873
18888
  data.previousFormatting.forEach((style, offset) => {
18874
- fm.applyFormatting(data.start + offset, data.start + offset + 1, style);
18889
+ fm.setFormattingAt(data.start + offset, style, true);
18875
18890
  });
18876
18891
  }
18877
18892
  redoFormat(mutation) {
@@ -23088,6 +23103,144 @@ class PCEditor extends EventEmitter {
23088
23103
  return null;
23089
23104
  }
23090
23105
  // ============================================
23106
+ // Text Box Update Operations
23107
+ // ============================================
23108
+ /**
23109
+ * Update properties of a text box.
23110
+ * @param textBoxId The ID of the text box to update
23111
+ * @param updates The properties to update
23112
+ */
23113
+ updateTextBox(textBoxId, updates) {
23114
+ if (!this._isReady)
23115
+ return false;
23116
+ // Find the text box in all flowing contents
23117
+ const textBox = this.findTextBoxById(textBoxId);
23118
+ if (!textBox) {
23119
+ console.warn(`[PCEditor.updateTextBox] Text box not found: ${textBoxId}`);
23120
+ return false;
23121
+ }
23122
+ // Apply updates
23123
+ if (updates.position !== undefined) {
23124
+ textBox.position = updates.position;
23125
+ }
23126
+ if (updates.relativeOffset !== undefined) {
23127
+ textBox.relativeOffset = updates.relativeOffset;
23128
+ }
23129
+ if (updates.backgroundColor !== undefined) {
23130
+ textBox.backgroundColor = updates.backgroundColor;
23131
+ }
23132
+ if (updates.border !== undefined) {
23133
+ // Merge with existing border
23134
+ const existingBorder = textBox.border;
23135
+ textBox.border = {
23136
+ top: updates.border.top || existingBorder.top,
23137
+ right: updates.border.right || existingBorder.right,
23138
+ bottom: updates.border.bottom || existingBorder.bottom,
23139
+ left: updates.border.left || existingBorder.left
23140
+ };
23141
+ }
23142
+ if (updates.padding !== undefined) {
23143
+ textBox.padding = updates.padding;
23144
+ }
23145
+ this.render();
23146
+ this.emit('textbox-updated', { textBoxId, updates });
23147
+ return true;
23148
+ }
23149
+ /**
23150
+ * Find a text box by ID across all flowing contents.
23151
+ */
23152
+ findTextBoxById(textBoxId) {
23153
+ const flowingContents = [
23154
+ this.document.bodyFlowingContent,
23155
+ this.document.headerFlowingContent,
23156
+ this.document.footerFlowingContent
23157
+ ].filter(Boolean);
23158
+ for (const flowingContent of flowingContents) {
23159
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23160
+ for (const [, obj] of embeddedObjects.entries()) {
23161
+ if (obj.id === textBoxId && obj instanceof TextBoxObject) {
23162
+ return obj;
23163
+ }
23164
+ }
23165
+ }
23166
+ return null;
23167
+ }
23168
+ // ============================================
23169
+ // Image Update Operations
23170
+ // ============================================
23171
+ /**
23172
+ * Update properties of an image.
23173
+ * @param imageId The ID of the image to update
23174
+ * @param updates The properties to update
23175
+ */
23176
+ updateImage(imageId, updates) {
23177
+ if (!this._isReady)
23178
+ return false;
23179
+ // Find the image in all flowing contents
23180
+ const image = this.findImageById(imageId);
23181
+ if (!image) {
23182
+ console.warn(`[PCEditor.updateImage] Image not found: ${imageId}`);
23183
+ return false;
23184
+ }
23185
+ // Apply updates
23186
+ if (updates.position !== undefined) {
23187
+ image.position = updates.position;
23188
+ }
23189
+ if (updates.relativeOffset !== undefined) {
23190
+ image.relativeOffset = updates.relativeOffset;
23191
+ }
23192
+ if (updates.fit !== undefined) {
23193
+ image.fit = updates.fit;
23194
+ }
23195
+ if (updates.resizeMode !== undefined) {
23196
+ image.resizeMode = updates.resizeMode;
23197
+ }
23198
+ if (updates.alt !== undefined) {
23199
+ image.alt = updates.alt;
23200
+ }
23201
+ this.render();
23202
+ this.emit('image-updated', { imageId, updates });
23203
+ return true;
23204
+ }
23205
+ /**
23206
+ * Set the source of an image.
23207
+ * @param imageId The ID of the image
23208
+ * @param dataUrl The data URL of the new image source
23209
+ * @param options Optional sizing options
23210
+ */
23211
+ setImageSource(imageId, dataUrl, options) {
23212
+ if (!this._isReady)
23213
+ return false;
23214
+ const image = this.findImageById(imageId);
23215
+ if (!image) {
23216
+ console.warn(`[PCEditor.setImageSource] Image not found: ${imageId}`);
23217
+ return false;
23218
+ }
23219
+ image.setSource(dataUrl, options);
23220
+ this.render();
23221
+ this.emit('image-source-changed', { imageId });
23222
+ return true;
23223
+ }
23224
+ /**
23225
+ * Find an image by ID across all flowing contents.
23226
+ */
23227
+ findImageById(imageId) {
23228
+ const flowingContents = [
23229
+ this.document.bodyFlowingContent,
23230
+ this.document.headerFlowingContent,
23231
+ this.document.footerFlowingContent
23232
+ ].filter(Boolean);
23233
+ for (const flowingContent of flowingContents) {
23234
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23235
+ for (const [, obj] of embeddedObjects.entries()) {
23236
+ if (obj.id === imageId && obj instanceof ImageObject) {
23237
+ return obj;
23238
+ }
23239
+ }
23240
+ }
23241
+ return null;
23242
+ }
23243
+ // ============================================
23091
23244
  // Table Structure Operations (with undo support)
23092
23245
  // ============================================
23093
23246
  /**
@@ -25158,5 +25311,2624 @@ class VerticalRuler extends RulerControl {
25158
25311
  }
25159
25312
  }
25160
25313
 
25161
- export { BaseControl, BaseEmbeddedObject, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, HeaderTextRegion, HorizontalRuler, HtmlConverter, ImageObject, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RulerControl, SubstitutionFieldManager, TableCell, TableObject, TableRow, TextBoxObject, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler };
25314
+ /**
25315
+ * BasePane - Abstract base class for editor property panes.
25316
+ *
25317
+ * Panes are property editors that work with PCEditor via the public API only.
25318
+ * They are content-only (no title bar) for flexible layout by consumers.
25319
+ */
25320
+ /**
25321
+ * Abstract base class for editor panes.
25322
+ */
25323
+ class BasePane extends BaseControl {
25324
+ constructor(id, options = {}) {
25325
+ super(id, options);
25326
+ this.sectionElement = null;
25327
+ this.className = options.className || '';
25328
+ }
25329
+ /**
25330
+ * Attach the pane to an editor.
25331
+ */
25332
+ attach(options) {
25333
+ // Store the section element if provided
25334
+ this.sectionElement = options.sectionElement || null;
25335
+ super.attach(options);
25336
+ }
25337
+ /**
25338
+ * Show the pane (and section element if provided).
25339
+ */
25340
+ show() {
25341
+ this._isVisible = true;
25342
+ if (this.sectionElement) {
25343
+ this.sectionElement.style.display = '';
25344
+ }
25345
+ if (this.element) {
25346
+ this.element.style.display = '';
25347
+ this.update();
25348
+ }
25349
+ this.emit('visibility-changed', { visible: true });
25350
+ }
25351
+ /**
25352
+ * Hide the pane (and section element if provided).
25353
+ */
25354
+ hide() {
25355
+ this._isVisible = false;
25356
+ if (this.sectionElement) {
25357
+ this.sectionElement.style.display = 'none';
25358
+ }
25359
+ if (this.element) {
25360
+ this.element.style.display = 'none';
25361
+ }
25362
+ this.emit('visibility-changed', { visible: false });
25363
+ }
25364
+ /**
25365
+ * Create a form group element with label.
25366
+ */
25367
+ createFormGroup(label, inputElement, options) {
25368
+ const group = document.createElement('div');
25369
+ group.className = 'pc-pane-form-group';
25370
+ if (options?.inline) {
25371
+ group.classList.add('pc-pane-form-group--inline');
25372
+ }
25373
+ const labelEl = document.createElement('label');
25374
+ labelEl.className = 'pc-pane-label';
25375
+ labelEl.textContent = label;
25376
+ group.appendChild(labelEl);
25377
+ group.appendChild(inputElement);
25378
+ if (options?.hint) {
25379
+ const hintEl = document.createElement('span');
25380
+ hintEl.className = 'pc-pane-hint';
25381
+ hintEl.textContent = options.hint;
25382
+ group.appendChild(hintEl);
25383
+ }
25384
+ return group;
25385
+ }
25386
+ /**
25387
+ * Create a text input element.
25388
+ */
25389
+ createTextInput(options) {
25390
+ const input = document.createElement('input');
25391
+ input.type = options?.type || 'text';
25392
+ input.className = 'pc-pane-input';
25393
+ if (options?.placeholder) {
25394
+ input.placeholder = options.placeholder;
25395
+ }
25396
+ if (options?.value !== undefined) {
25397
+ input.value = options.value;
25398
+ }
25399
+ return input;
25400
+ }
25401
+ /**
25402
+ * Create a number input element.
25403
+ */
25404
+ createNumberInput(options) {
25405
+ const input = document.createElement('input');
25406
+ input.type = 'number';
25407
+ input.className = 'pc-pane-input pc-pane-input--number';
25408
+ if (options?.min !== undefined)
25409
+ input.min = String(options.min);
25410
+ if (options?.max !== undefined)
25411
+ input.max = String(options.max);
25412
+ if (options?.step !== undefined)
25413
+ input.step = String(options.step);
25414
+ if (options?.value !== undefined)
25415
+ input.value = String(options.value);
25416
+ return input;
25417
+ }
25418
+ /**
25419
+ * Create a select element with options.
25420
+ */
25421
+ createSelect(optionsList, selectedValue) {
25422
+ const select = document.createElement('select');
25423
+ select.className = 'pc-pane-select';
25424
+ for (const opt of optionsList) {
25425
+ const option = document.createElement('option');
25426
+ option.value = opt.value;
25427
+ option.textContent = opt.label;
25428
+ if (opt.value === selectedValue) {
25429
+ option.selected = true;
25430
+ }
25431
+ select.appendChild(option);
25432
+ }
25433
+ return select;
25434
+ }
25435
+ /**
25436
+ * Create a color input element.
25437
+ */
25438
+ createColorInput(value) {
25439
+ const input = document.createElement('input');
25440
+ input.type = 'color';
25441
+ input.className = 'pc-pane-color';
25442
+ if (value) {
25443
+ input.value = value;
25444
+ }
25445
+ return input;
25446
+ }
25447
+ /**
25448
+ * Create a checkbox element.
25449
+ */
25450
+ createCheckbox(label, checked) {
25451
+ const wrapper = document.createElement('label');
25452
+ wrapper.className = 'pc-pane-checkbox';
25453
+ const input = document.createElement('input');
25454
+ input.type = 'checkbox';
25455
+ if (checked) {
25456
+ input.checked = true;
25457
+ }
25458
+ const span = document.createElement('span');
25459
+ span.textContent = label;
25460
+ wrapper.appendChild(input);
25461
+ wrapper.appendChild(span);
25462
+ return wrapper;
25463
+ }
25464
+ /**
25465
+ * Create a button element.
25466
+ */
25467
+ createButton(label, options) {
25468
+ const button = document.createElement('button');
25469
+ button.type = 'button';
25470
+ button.className = 'pc-pane-button';
25471
+ if (options?.variant) {
25472
+ button.classList.add(`pc-pane-button--${options.variant}`);
25473
+ }
25474
+ button.textContent = label;
25475
+ return button;
25476
+ }
25477
+ /**
25478
+ * Create a button group container.
25479
+ */
25480
+ createButtonGroup() {
25481
+ const group = document.createElement('div');
25482
+ group.className = 'pc-pane-button-group';
25483
+ return group;
25484
+ }
25485
+ /**
25486
+ * Create a section divider with optional label.
25487
+ */
25488
+ createSection(label) {
25489
+ const section = document.createElement('div');
25490
+ section.className = 'pc-pane-section';
25491
+ if (label) {
25492
+ const labelEl = document.createElement('div');
25493
+ labelEl.className = 'pc-pane-section-label';
25494
+ labelEl.textContent = label;
25495
+ section.appendChild(labelEl);
25496
+ }
25497
+ return section;
25498
+ }
25499
+ /**
25500
+ * Create a row container for inline elements.
25501
+ */
25502
+ createRow() {
25503
+ const row = document.createElement('div');
25504
+ row.className = 'pc-pane-row';
25505
+ return row;
25506
+ }
25507
+ /**
25508
+ * Create a hint/info text element.
25509
+ */
25510
+ createHint(text) {
25511
+ const hint = document.createElement('div');
25512
+ hint.className = 'pc-pane-hint';
25513
+ hint.textContent = text;
25514
+ return hint;
25515
+ }
25516
+ /**
25517
+ * Add immediate apply listener for text inputs (blur + Enter).
25518
+ */
25519
+ addImmediateApplyListener(element, handler) {
25520
+ const apply = () => {
25521
+ handler(element.value);
25522
+ };
25523
+ // Selects and color inputs: apply on change
25524
+ if (element instanceof HTMLSelectElement ||
25525
+ (element instanceof HTMLInputElement && element.type === 'color')) {
25526
+ element.addEventListener('change', apply);
25527
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25528
+ }
25529
+ else {
25530
+ // Text/number inputs: apply on blur or Enter
25531
+ element.addEventListener('blur', apply);
25532
+ const keyHandler = (e) => {
25533
+ if (e.key === 'Enter') {
25534
+ e.preventDefault();
25535
+ apply();
25536
+ }
25537
+ };
25538
+ element.addEventListener('keydown', keyHandler);
25539
+ this.eventCleanup.push(() => {
25540
+ element.removeEventListener('blur', apply);
25541
+ element.removeEventListener('keydown', keyHandler);
25542
+ });
25543
+ }
25544
+ }
25545
+ /**
25546
+ * Add immediate apply listener for checkbox inputs.
25547
+ */
25548
+ addCheckboxListener(element, handler) {
25549
+ const apply = () => handler(element.checked);
25550
+ element.addEventListener('change', apply);
25551
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25552
+ }
25553
+ /**
25554
+ * Add button click handler with focus steal prevention.
25555
+ */
25556
+ addButtonListener(button, handler) {
25557
+ // Prevent focus steal on mousedown
25558
+ const preventFocus = (e) => {
25559
+ e.preventDefault();
25560
+ this.saveEditorContext();
25561
+ };
25562
+ button.addEventListener('mousedown', preventFocus);
25563
+ button.addEventListener('click', handler);
25564
+ this.eventCleanup.push(() => {
25565
+ button.removeEventListener('mousedown', preventFocus);
25566
+ button.removeEventListener('click', handler);
25567
+ });
25568
+ }
25569
+ /**
25570
+ * Save editor context before UI elements steal focus.
25571
+ */
25572
+ saveEditorContext() {
25573
+ if (this.editor) {
25574
+ this.editor.saveEditingContext();
25575
+ }
25576
+ }
25577
+ /**
25578
+ * Final createElement that wraps content in pane structure.
25579
+ * Content-only, no title bar.
25580
+ */
25581
+ createElement() {
25582
+ const wrapper = document.createElement('div');
25583
+ wrapper.className = 'pc-pane';
25584
+ if (this.className) {
25585
+ wrapper.classList.add(this.className);
25586
+ }
25587
+ wrapper.setAttribute('data-pane-id', this.id);
25588
+ const content = this.createContent();
25589
+ wrapper.appendChild(content);
25590
+ return wrapper;
25591
+ }
25592
+ }
25593
+
25594
+ /**
25595
+ * DocumentInfoPane - Read-only document information display.
25596
+ *
25597
+ * Shows:
25598
+ * - Page count
25599
+ * - Page size
25600
+ * - Page orientation
25601
+ */
25602
+ class DocumentInfoPane extends BasePane {
25603
+ constructor(id = 'document-info') {
25604
+ super(id, { className: 'pc-pane-document-info' });
25605
+ this.pageCountEl = null;
25606
+ this.pageSizeEl = null;
25607
+ this.pageOrientationEl = null;
25608
+ }
25609
+ attach(options) {
25610
+ super.attach(options);
25611
+ // Subscribe to document changes
25612
+ if (this.editor) {
25613
+ const updateHandler = () => this.update();
25614
+ this.editor.on('document-changed', updateHandler);
25615
+ this.editor.on('page-added', updateHandler);
25616
+ this.editor.on('page-removed', updateHandler);
25617
+ this.eventCleanup.push(() => {
25618
+ this.editor?.off('document-changed', updateHandler);
25619
+ this.editor?.off('page-added', updateHandler);
25620
+ this.editor?.off('page-removed', updateHandler);
25621
+ });
25622
+ // Initial update
25623
+ this.update();
25624
+ }
25625
+ }
25626
+ createContent() {
25627
+ const container = document.createElement('div');
25628
+ container.className = 'pc-pane-info-list';
25629
+ // Page count
25630
+ const countRow = this.createInfoRow('Pages', '0');
25631
+ this.pageCountEl = countRow.querySelector('.pc-pane-info-value');
25632
+ container.appendChild(countRow);
25633
+ // Page size
25634
+ const sizeRow = this.createInfoRow('Size', '-');
25635
+ this.pageSizeEl = sizeRow.querySelector('.pc-pane-info-value');
25636
+ container.appendChild(sizeRow);
25637
+ // Page orientation
25638
+ const orientationRow = this.createInfoRow('Orientation', '-');
25639
+ this.pageOrientationEl = orientationRow.querySelector('.pc-pane-info-value');
25640
+ container.appendChild(orientationRow);
25641
+ return container;
25642
+ }
25643
+ createInfoRow(label, value) {
25644
+ const row = document.createElement('div');
25645
+ row.className = 'pc-pane-info';
25646
+ const labelEl = document.createElement('span');
25647
+ labelEl.className = 'pc-pane-info-label';
25648
+ labelEl.textContent = label;
25649
+ const valueEl = document.createElement('span');
25650
+ valueEl.className = 'pc-pane-info-value';
25651
+ valueEl.textContent = value;
25652
+ row.appendChild(labelEl);
25653
+ row.appendChild(valueEl);
25654
+ return row;
25655
+ }
25656
+ /**
25657
+ * Update the displayed information from the editor.
25658
+ */
25659
+ update() {
25660
+ if (!this.editor)
25661
+ return;
25662
+ const doc = this.editor.getDocument();
25663
+ if (this.pageCountEl) {
25664
+ this.pageCountEl.textContent = doc.pages.length.toString();
25665
+ }
25666
+ if (this.pageSizeEl && doc.settings) {
25667
+ this.pageSizeEl.textContent = doc.settings.pageSize;
25668
+ }
25669
+ if (this.pageOrientationEl && doc.settings) {
25670
+ const orientation = doc.settings.pageOrientation;
25671
+ this.pageOrientationEl.textContent =
25672
+ orientation.charAt(0).toUpperCase() + orientation.slice(1);
25673
+ }
25674
+ }
25675
+ }
25676
+
25677
+ /**
25678
+ * ViewSettingsPane - Toggle buttons for view options.
25679
+ *
25680
+ * Toggles:
25681
+ * - Rulers (requires external callback since rulers are optional controls)
25682
+ * - Control characters
25683
+ * - Margin lines
25684
+ * - Grid
25685
+ */
25686
+ class ViewSettingsPane extends BasePane {
25687
+ constructor(id = 'view-settings', options = {}) {
25688
+ super(id, { className: 'pc-pane-view-settings', ...options });
25689
+ this.rulersBtn = null;
25690
+ this.controlCharsBtn = null;
25691
+ this.marginLinesBtn = null;
25692
+ this.gridBtn = null;
25693
+ this.onToggleRulers = options.onToggleRulers;
25694
+ this.rulersVisible = options.rulersVisible ?? true;
25695
+ }
25696
+ attach(options) {
25697
+ super.attach(options);
25698
+ // Subscribe to editor events
25699
+ if (this.editor) {
25700
+ const updateHandler = () => this.updateButtonStates();
25701
+ this.editor.on('grid-changed', updateHandler);
25702
+ this.editor.on('margin-lines-changed', updateHandler);
25703
+ this.editor.on('control-characters-changed', updateHandler);
25704
+ this.eventCleanup.push(() => {
25705
+ this.editor?.off('grid-changed', updateHandler);
25706
+ this.editor?.off('margin-lines-changed', updateHandler);
25707
+ this.editor?.off('control-characters-changed', updateHandler);
25708
+ });
25709
+ // Initial state
25710
+ this.updateButtonStates();
25711
+ }
25712
+ }
25713
+ createContent() {
25714
+ const container = document.createElement('div');
25715
+ container.className = 'pc-pane-button-group pc-pane-view-toggles';
25716
+ // Rulers toggle (only if callback provided)
25717
+ if (this.onToggleRulers) {
25718
+ this.rulersBtn = this.createToggleButton('Rulers', this.rulersVisible);
25719
+ this.addButtonListener(this.rulersBtn, () => this.toggleRulers());
25720
+ container.appendChild(this.rulersBtn);
25721
+ }
25722
+ // Control characters toggle
25723
+ this.controlCharsBtn = this.createToggleButton('Control Chars', false);
25724
+ this.addButtonListener(this.controlCharsBtn, () => this.toggleControlChars());
25725
+ container.appendChild(this.controlCharsBtn);
25726
+ // Margin lines toggle
25727
+ this.marginLinesBtn = this.createToggleButton('Margin Lines', true);
25728
+ this.addButtonListener(this.marginLinesBtn, () => this.toggleMarginLines());
25729
+ container.appendChild(this.marginLinesBtn);
25730
+ // Grid toggle
25731
+ this.gridBtn = this.createToggleButton('Grid', true);
25732
+ this.addButtonListener(this.gridBtn, () => this.toggleGrid());
25733
+ container.appendChild(this.gridBtn);
25734
+ return container;
25735
+ }
25736
+ createToggleButton(label, active) {
25737
+ const button = document.createElement('button');
25738
+ button.type = 'button';
25739
+ button.className = 'pc-pane-toggle';
25740
+ if (active) {
25741
+ button.classList.add('pc-pane-toggle--active');
25742
+ }
25743
+ button.textContent = label;
25744
+ button.title = `Toggle ${label}`;
25745
+ return button;
25746
+ }
25747
+ toggleRulers() {
25748
+ if (this.onToggleRulers) {
25749
+ this.onToggleRulers();
25750
+ this.rulersVisible = !this.rulersVisible;
25751
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', this.rulersVisible);
25752
+ }
25753
+ }
25754
+ toggleControlChars() {
25755
+ if (!this.editor)
25756
+ return;
25757
+ const current = this.editor.getShowControlCharacters();
25758
+ this.editor.setShowControlCharacters(!current);
25759
+ }
25760
+ toggleMarginLines() {
25761
+ if (!this.editor)
25762
+ return;
25763
+ const current = this.editor.getShowMarginLines();
25764
+ this.editor.setShowMarginLines(!current);
25765
+ }
25766
+ toggleGrid() {
25767
+ if (!this.editor)
25768
+ return;
25769
+ const current = this.editor.getShowGrid();
25770
+ this.editor.setShowGrid(!current);
25771
+ }
25772
+ updateButtonStates() {
25773
+ if (!this.editor)
25774
+ return;
25775
+ if (this.controlCharsBtn) {
25776
+ this.controlCharsBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowControlCharacters());
25777
+ }
25778
+ if (this.marginLinesBtn) {
25779
+ this.marginLinesBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowMarginLines());
25780
+ }
25781
+ if (this.gridBtn) {
25782
+ this.gridBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowGrid());
25783
+ }
25784
+ }
25785
+ /**
25786
+ * Update ruler button state externally (since rulers are external controls).
25787
+ */
25788
+ setRulersVisible(visible) {
25789
+ this.rulersVisible = visible;
25790
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', visible);
25791
+ }
25792
+ /**
25793
+ * Update the pane from current editor state.
25794
+ */
25795
+ update() {
25796
+ this.updateButtonStates();
25797
+ }
25798
+ }
25799
+
25800
+ /**
25801
+ * DocumentSettingsPane - Edit margins, page size, and orientation.
25802
+ *
25803
+ * Uses the PCEditor public API:
25804
+ * - editor.getDocumentSettings()
25805
+ * - editor.updateDocumentSettings()
25806
+ */
25807
+ class DocumentSettingsPane extends BasePane {
25808
+ constructor(id = 'document-settings') {
25809
+ super(id, { className: 'pc-pane-document-settings' });
25810
+ this.marginTopInput = null;
25811
+ this.marginRightInput = null;
25812
+ this.marginBottomInput = null;
25813
+ this.marginLeftInput = null;
25814
+ this.pageSizeSelect = null;
25815
+ this.orientationSelect = null;
25816
+ }
25817
+ attach(options) {
25818
+ super.attach(options);
25819
+ // Load current settings
25820
+ if (this.editor) {
25821
+ this.loadSettings();
25822
+ // Subscribe to document changes
25823
+ const updateHandler = () => this.loadSettings();
25824
+ this.editor.on('document-changed', updateHandler);
25825
+ this.eventCleanup.push(() => {
25826
+ this.editor?.off('document-changed', updateHandler);
25827
+ });
25828
+ }
25829
+ }
25830
+ createContent() {
25831
+ const container = document.createElement('div');
25832
+ // Margins section
25833
+ const marginsSection = this.createSection('Margins (mm)');
25834
+ const marginsGrid = document.createElement('div');
25835
+ marginsGrid.className = 'pc-pane-margins-grid';
25836
+ this.marginTopInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25837
+ this.marginRightInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25838
+ this.marginBottomInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25839
+ this.marginLeftInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25840
+ marginsGrid.appendChild(this.createFormGroup('Top', this.marginTopInput, { inline: true }));
25841
+ marginsGrid.appendChild(this.createFormGroup('Right', this.marginRightInput, { inline: true }));
25842
+ marginsGrid.appendChild(this.createFormGroup('Bottom', this.marginBottomInput, { inline: true }));
25843
+ marginsGrid.appendChild(this.createFormGroup('Left', this.marginLeftInput, { inline: true }));
25844
+ marginsSection.appendChild(marginsGrid);
25845
+ // Apply margins button
25846
+ const applyMarginsBtn = this.createButton('Apply Margins');
25847
+ this.addButtonListener(applyMarginsBtn, () => this.applyMargins());
25848
+ marginsSection.appendChild(applyMarginsBtn);
25849
+ container.appendChild(marginsSection);
25850
+ // Page size section
25851
+ const pageSizeSection = this.createSection();
25852
+ this.pageSizeSelect = this.createSelect([
25853
+ { value: 'A4', label: 'A4' },
25854
+ { value: 'Letter', label: 'Letter' },
25855
+ { value: 'Legal', label: 'Legal' },
25856
+ { value: 'A3', label: 'A3' }
25857
+ ], 'A4');
25858
+ this.addImmediateApplyListener(this.pageSizeSelect, () => this.applyPageSettings());
25859
+ pageSizeSection.appendChild(this.createFormGroup('Page Size', this.pageSizeSelect));
25860
+ container.appendChild(pageSizeSection);
25861
+ // Orientation section
25862
+ const orientationSection = this.createSection();
25863
+ this.orientationSelect = this.createSelect([
25864
+ { value: 'portrait', label: 'Portrait' },
25865
+ { value: 'landscape', label: 'Landscape' }
25866
+ ], 'portrait');
25867
+ this.addImmediateApplyListener(this.orientationSelect, () => this.applyPageSettings());
25868
+ orientationSection.appendChild(this.createFormGroup('Orientation', this.orientationSelect));
25869
+ container.appendChild(orientationSection);
25870
+ return container;
25871
+ }
25872
+ loadSettings() {
25873
+ if (!this.editor)
25874
+ return;
25875
+ try {
25876
+ const settings = this.editor.getDocumentSettings();
25877
+ if (this.marginTopInput) {
25878
+ this.marginTopInput.value = settings.margins.top.toString();
25879
+ }
25880
+ if (this.marginRightInput) {
25881
+ this.marginRightInput.value = settings.margins.right.toString();
25882
+ }
25883
+ if (this.marginBottomInput) {
25884
+ this.marginBottomInput.value = settings.margins.bottom.toString();
25885
+ }
25886
+ if (this.marginLeftInput) {
25887
+ this.marginLeftInput.value = settings.margins.left.toString();
25888
+ }
25889
+ if (this.pageSizeSelect) {
25890
+ this.pageSizeSelect.value = settings.pageSize;
25891
+ }
25892
+ if (this.orientationSelect) {
25893
+ this.orientationSelect.value = settings.pageOrientation;
25894
+ }
25895
+ }
25896
+ catch (error) {
25897
+ console.error('Failed to load document settings:', error);
25898
+ }
25899
+ }
25900
+ applyMargins() {
25901
+ if (!this.editor)
25902
+ return;
25903
+ const margins = {
25904
+ top: parseFloat(this.marginTopInput?.value || '20'),
25905
+ right: parseFloat(this.marginRightInput?.value || '20'),
25906
+ bottom: parseFloat(this.marginBottomInput?.value || '20'),
25907
+ left: parseFloat(this.marginLeftInput?.value || '20')
25908
+ };
25909
+ try {
25910
+ this.editor.updateDocumentSettings({ margins });
25911
+ }
25912
+ catch (error) {
25913
+ console.error('Failed to update margins:', error);
25914
+ }
25915
+ }
25916
+ applyPageSettings() {
25917
+ if (!this.editor)
25918
+ return;
25919
+ const settings = {};
25920
+ if (this.pageSizeSelect) {
25921
+ settings.pageSize = this.pageSizeSelect.value;
25922
+ }
25923
+ if (this.orientationSelect) {
25924
+ settings.pageOrientation = this.orientationSelect.value;
25925
+ }
25926
+ try {
25927
+ this.editor.updateDocumentSettings(settings);
25928
+ }
25929
+ catch (error) {
25930
+ console.error('Failed to update page settings:', error);
25931
+ }
25932
+ }
25933
+ /**
25934
+ * Update the pane from current editor state.
25935
+ */
25936
+ update() {
25937
+ this.loadSettings();
25938
+ }
25939
+ }
25940
+
25941
+ /**
25942
+ * MergeDataPane - JSON data input for mail merge/substitution.
25943
+ *
25944
+ * Uses the PCEditor public API:
25945
+ * - editor.applyMergeData()
25946
+ */
25947
+ class MergeDataPane extends BasePane {
25948
+ constructor(id = 'merge-data', options = {}) {
25949
+ super(id, { className: 'pc-pane-merge-data', ...options });
25950
+ this.textarea = null;
25951
+ this.errorHint = null;
25952
+ this.initialData = options.initialData;
25953
+ this.placeholder = options.placeholder || '{"customerName": "John Doe", "orderNumber": "12345"}';
25954
+ this.rows = options.rows ?? 10;
25955
+ this.onApply = options.onApply;
25956
+ }
25957
+ createContent() {
25958
+ const container = document.createElement('div');
25959
+ // Textarea for JSON
25960
+ const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
25961
+ container.appendChild(textareaGroup);
25962
+ // Error hint (hidden by default)
25963
+ this.errorHint = this.createHint('');
25964
+ this.errorHint.style.display = 'none';
25965
+ this.errorHint.style.color = '#dc3545';
25966
+ container.appendChild(this.errorHint);
25967
+ // Apply button
25968
+ const applyBtn = this.createButton('Apply Merge Data', { variant: 'primary' });
25969
+ this.addButtonListener(applyBtn, () => this.applyMergeData());
25970
+ container.appendChild(applyBtn);
25971
+ return container;
25972
+ }
25973
+ createTextarea() {
25974
+ this.textarea = document.createElement('textarea');
25975
+ this.textarea.className = 'pc-pane-textarea pc-pane-merge-data-input';
25976
+ this.textarea.rows = this.rows;
25977
+ this.textarea.placeholder = this.placeholder;
25978
+ this.textarea.spellcheck = false;
25979
+ if (this.initialData) {
25980
+ this.textarea.value = JSON.stringify(this.initialData, null, 2);
25981
+ }
25982
+ // Clear error on input
25983
+ this.textarea.addEventListener('input', () => {
25984
+ if (this.errorHint) {
25985
+ this.errorHint.style.display = 'none';
25986
+ }
25987
+ });
25988
+ return this.textarea;
25989
+ }
25990
+ applyMergeData() {
25991
+ if (!this.editor || !this.textarea)
25992
+ return;
25993
+ try {
25994
+ const mergeData = JSON.parse(this.textarea.value);
25995
+ this.editor.applyMergeData(mergeData);
25996
+ if (this.errorHint) {
25997
+ this.errorHint.style.display = 'none';
25998
+ }
25999
+ this.onApply?.(true);
26000
+ }
26001
+ catch (error) {
26002
+ const err = error instanceof Error ? error : new Error(String(error));
26003
+ if (this.errorHint) {
26004
+ if (error instanceof SyntaxError) {
26005
+ this.errorHint.textContent = 'Invalid JSON syntax';
26006
+ }
26007
+ else {
26008
+ this.errorHint.textContent = err.message;
26009
+ }
26010
+ this.errorHint.style.display = 'block';
26011
+ }
26012
+ this.onApply?.(false, err);
26013
+ }
26014
+ }
26015
+ /**
26016
+ * Get the current JSON data from the textarea.
26017
+ * Returns null if the JSON is invalid.
26018
+ */
26019
+ getData() {
26020
+ if (!this.textarea)
26021
+ return null;
26022
+ try {
26023
+ return JSON.parse(this.textarea.value);
26024
+ }
26025
+ catch {
26026
+ return null;
26027
+ }
26028
+ }
26029
+ /**
26030
+ * Set the JSON data in the textarea.
26031
+ */
26032
+ setData(data) {
26033
+ if (this.textarea) {
26034
+ this.textarea.value = JSON.stringify(data, null, 2);
26035
+ if (this.errorHint) {
26036
+ this.errorHint.style.display = 'none';
26037
+ }
26038
+ }
26039
+ }
26040
+ /**
26041
+ * Update the pane (no-op for MergeDataPane as it doesn't track editor state).
26042
+ */
26043
+ update() {
26044
+ // MergeDataPane doesn't need to update from editor state
26045
+ }
26046
+ }
26047
+
26048
+ /**
26049
+ * FormattingPane - Text formatting controls.
26050
+ *
26051
+ * Controls:
26052
+ * - Bold/Italic toggles
26053
+ * - Alignment buttons (left, center, right, justify)
26054
+ * - List buttons (bullet, numbered, indent, outdent)
26055
+ * - Font family/size dropdowns
26056
+ * - Text color and highlight color pickers
26057
+ *
26058
+ * Uses the PCEditor public API:
26059
+ * - editor.getUnifiedFormattingAtCursor()
26060
+ * - editor.applyFormattingWithFallback()
26061
+ * - editor.setPendingFormatting()
26062
+ * - editor.getSavedOrCurrentSelection()
26063
+ * - editor.getUnifiedAlignmentAtCursor()
26064
+ * - editor.setUnifiedAlignment()
26065
+ * - editor.toggleBulletList()
26066
+ * - editor.toggleNumberedList()
26067
+ * - editor.indentParagraph()
26068
+ * - editor.outdentParagraph()
26069
+ * - editor.getListFormatting()
26070
+ */
26071
+ const DEFAULT_FONT_FAMILIES = [
26072
+ 'Arial',
26073
+ 'Times New Roman',
26074
+ 'Georgia',
26075
+ 'Verdana',
26076
+ 'Courier New'
26077
+ ];
26078
+ const DEFAULT_FONT_SIZES = [10, 12, 14, 16, 18, 20, 24, 28, 32, 36];
26079
+ class FormattingPane extends BasePane {
26080
+ constructor(id = 'formatting', options = {}) {
26081
+ super(id, { className: 'pc-pane-formatting', ...options });
26082
+ // Style toggles
26083
+ this.boldBtn = null;
26084
+ this.italicBtn = null;
26085
+ // Alignment buttons
26086
+ this.alignLeftBtn = null;
26087
+ this.alignCenterBtn = null;
26088
+ this.alignRightBtn = null;
26089
+ this.alignJustifyBtn = null;
26090
+ // List buttons
26091
+ this.bulletListBtn = null;
26092
+ this.numberedListBtn = null;
26093
+ this.indentBtn = null;
26094
+ this.outdentBtn = null;
26095
+ // Font controls
26096
+ this.fontFamilySelect = null;
26097
+ this.fontSizeSelect = null;
26098
+ this.colorInput = null;
26099
+ this.highlightInput = null;
26100
+ this.fontFamilies = options.fontFamilies ?? DEFAULT_FONT_FAMILIES;
26101
+ this.fontSizes = options.fontSizes ?? DEFAULT_FONT_SIZES;
26102
+ }
26103
+ attach(options) {
26104
+ super.attach(options);
26105
+ if (this.editor) {
26106
+ // Update on cursor/selection changes
26107
+ const updateHandler = () => this.updateFromEditor();
26108
+ this.editor.on('cursor-changed', updateHandler);
26109
+ this.editor.on('selection-changed', updateHandler);
26110
+ this.editor.on('text-changed', updateHandler);
26111
+ this.editor.on('formatting-changed', updateHandler);
26112
+ this.eventCleanup.push(() => {
26113
+ this.editor?.off('cursor-changed', updateHandler);
26114
+ this.editor?.off('selection-changed', updateHandler);
26115
+ this.editor?.off('text-changed', updateHandler);
26116
+ this.editor?.off('formatting-changed', updateHandler);
26117
+ });
26118
+ // Initial update
26119
+ this.updateFromEditor();
26120
+ }
26121
+ }
26122
+ createContent() {
26123
+ const container = document.createElement('div');
26124
+ // Style section (Bold, Italic)
26125
+ const styleSection = this.createSection('Style');
26126
+ const styleGroup = this.createButtonGroup();
26127
+ this.boldBtn = this.createButton('B');
26128
+ this.boldBtn.title = 'Bold';
26129
+ this.boldBtn.style.fontWeight = 'bold';
26130
+ this.addButtonListener(this.boldBtn, () => this.toggleBold());
26131
+ this.italicBtn = this.createButton('I');
26132
+ this.italicBtn.title = 'Italic';
26133
+ this.italicBtn.style.fontStyle = 'italic';
26134
+ this.addButtonListener(this.italicBtn, () => this.toggleItalic());
26135
+ styleGroup.appendChild(this.boldBtn);
26136
+ styleGroup.appendChild(this.italicBtn);
26137
+ styleSection.appendChild(styleGroup);
26138
+ container.appendChild(styleSection);
26139
+ // Alignment section
26140
+ const alignSection = this.createSection('Alignment');
26141
+ const alignGroup = this.createButtonGroup();
26142
+ this.alignLeftBtn = this.createButton('');
26143
+ this.alignLeftBtn.title = 'Align Left';
26144
+ this.alignLeftBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-left');
26145
+ this.addButtonListener(this.alignLeftBtn, () => this.setAlignment('left'));
26146
+ this.alignCenterBtn = this.createButton('');
26147
+ this.alignCenterBtn.title = 'Center';
26148
+ this.alignCenterBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-center');
26149
+ this.addButtonListener(this.alignCenterBtn, () => this.setAlignment('center'));
26150
+ this.alignRightBtn = this.createButton('');
26151
+ this.alignRightBtn.title = 'Align Right';
26152
+ this.alignRightBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-right');
26153
+ this.addButtonListener(this.alignRightBtn, () => this.setAlignment('right'));
26154
+ this.alignJustifyBtn = this.createButton('');
26155
+ this.alignJustifyBtn.title = 'Justify';
26156
+ this.alignJustifyBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-justify');
26157
+ this.addButtonListener(this.alignJustifyBtn, () => this.setAlignment('justify'));
26158
+ alignGroup.appendChild(this.alignLeftBtn);
26159
+ alignGroup.appendChild(this.alignCenterBtn);
26160
+ alignGroup.appendChild(this.alignRightBtn);
26161
+ alignGroup.appendChild(this.alignJustifyBtn);
26162
+ alignSection.appendChild(alignGroup);
26163
+ container.appendChild(alignSection);
26164
+ // Lists section
26165
+ const listsSection = this.createSection('Lists');
26166
+ const listsGroup = this.createButtonGroup();
26167
+ this.bulletListBtn = this.createButton('\u2022'); // •
26168
+ this.bulletListBtn.title = 'Bullet List';
26169
+ this.addButtonListener(this.bulletListBtn, () => this.toggleBulletList());
26170
+ this.numberedListBtn = this.createButton('1.');
26171
+ this.numberedListBtn.title = 'Numbered List';
26172
+ this.addButtonListener(this.numberedListBtn, () => this.toggleNumberedList());
26173
+ this.indentBtn = this.createButton('\u2192'); // →
26174
+ this.indentBtn.title = 'Increase Indent';
26175
+ this.addButtonListener(this.indentBtn, () => this.indent());
26176
+ this.outdentBtn = this.createButton('\u2190'); // ←
26177
+ this.outdentBtn.title = 'Decrease Indent';
26178
+ this.addButtonListener(this.outdentBtn, () => this.outdent());
26179
+ listsGroup.appendChild(this.bulletListBtn);
26180
+ listsGroup.appendChild(this.numberedListBtn);
26181
+ listsGroup.appendChild(this.indentBtn);
26182
+ listsGroup.appendChild(this.outdentBtn);
26183
+ listsSection.appendChild(listsGroup);
26184
+ container.appendChild(listsSection);
26185
+ // Font section
26186
+ const fontSection = this.createSection('Font');
26187
+ this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26188
+ this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26189
+ fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
26190
+ this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26191
+ this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26192
+ fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
26193
+ container.appendChild(fontSection);
26194
+ // Color section
26195
+ const colorSection = this.createSection('Color');
26196
+ const colorRow = this.createRow();
26197
+ const colorGroup = document.createElement('div');
26198
+ this.colorInput = this.createColorInput('#000000');
26199
+ this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26200
+ colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26201
+ colorRow.appendChild(colorGroup);
26202
+ const highlightGroup = document.createElement('div');
26203
+ this.highlightInput = this.createColorInput('#ffff00');
26204
+ this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26205
+ const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
26206
+ const clearHighlightBtn = this.createButton('Clear');
26207
+ clearHighlightBtn.className = 'pc-pane-button';
26208
+ clearHighlightBtn.style.marginLeft = '4px';
26209
+ this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26210
+ highlightForm.appendChild(clearHighlightBtn);
26211
+ highlightGroup.appendChild(highlightForm);
26212
+ colorRow.appendChild(highlightGroup);
26213
+ colorSection.appendChild(colorRow);
26214
+ container.appendChild(colorSection);
26215
+ return container;
26216
+ }
26217
+ updateFromEditor() {
26218
+ if (!this.editor)
26219
+ return;
26220
+ // Get formatting at cursor
26221
+ const formatting = this.editor.getUnifiedFormattingAtCursor();
26222
+ if (formatting) {
26223
+ // Update bold button
26224
+ this.boldBtn?.classList.toggle('pc-pane-button--active', formatting.fontWeight === 'bold');
26225
+ // Update italic button
26226
+ this.italicBtn?.classList.toggle('pc-pane-button--active', formatting.fontStyle === 'italic');
26227
+ // Update font family
26228
+ if (this.fontFamilySelect && formatting.fontFamily) {
26229
+ this.fontFamilySelect.value = formatting.fontFamily;
26230
+ }
26231
+ // Update font size
26232
+ if (this.fontSizeSelect && formatting.fontSize) {
26233
+ this.fontSizeSelect.value = formatting.fontSize.toString();
26234
+ }
26235
+ // Update color
26236
+ if (this.colorInput && formatting.color) {
26237
+ this.colorInput.value = formatting.color;
26238
+ }
26239
+ // Update highlight
26240
+ if (this.highlightInput && formatting.backgroundColor) {
26241
+ this.highlightInput.value = formatting.backgroundColor;
26242
+ }
26243
+ }
26244
+ // Update alignment buttons
26245
+ const alignment = this.editor.getUnifiedAlignmentAtCursor();
26246
+ this.updateAlignmentButtons(alignment);
26247
+ // Update list buttons
26248
+ this.updateListButtons();
26249
+ }
26250
+ updateAlignmentButtons(alignment) {
26251
+ const buttons = [
26252
+ { btn: this.alignLeftBtn, align: 'left' },
26253
+ { btn: this.alignCenterBtn, align: 'center' },
26254
+ { btn: this.alignRightBtn, align: 'right' },
26255
+ { btn: this.alignJustifyBtn, align: 'justify' }
26256
+ ];
26257
+ for (const { btn, align } of buttons) {
26258
+ btn?.classList.toggle('pc-pane-button--active', align === alignment);
26259
+ }
26260
+ }
26261
+ updateListButtons() {
26262
+ if (!this.editor)
26263
+ return;
26264
+ try {
26265
+ const listFormatting = this.editor.getListFormatting();
26266
+ if (listFormatting) {
26267
+ this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26268
+ this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26269
+ }
26270
+ }
26271
+ catch {
26272
+ // No text editing active
26273
+ }
26274
+ }
26275
+ getSelection() {
26276
+ if (!this.editor)
26277
+ return null;
26278
+ return this.editor.getSavedOrCurrentSelection();
26279
+ }
26280
+ applyFormatting(formatting) {
26281
+ if (!this.editor)
26282
+ return;
26283
+ const selection = this.getSelection();
26284
+ try {
26285
+ if (selection) {
26286
+ this.editor.applyFormattingWithFallback(selection.start, selection.end, formatting);
26287
+ }
26288
+ else {
26289
+ this.editor.setPendingFormatting(formatting);
26290
+ }
26291
+ this.editor.clearSavedEditingContext();
26292
+ this.updateFromEditor();
26293
+ this.editor.enableTextInput();
26294
+ }
26295
+ catch (error) {
26296
+ console.error('Formatting error:', error);
26297
+ }
26298
+ }
26299
+ toggleBold() {
26300
+ const isActive = this.boldBtn?.classList.contains('pc-pane-button--active');
26301
+ this.applyFormatting({ fontWeight: isActive ? 'normal' : 'bold' });
26302
+ }
26303
+ toggleItalic() {
26304
+ const isActive = this.italicBtn?.classList.contains('pc-pane-button--active');
26305
+ this.applyFormatting({ fontStyle: isActive ? 'normal' : 'italic' });
26306
+ }
26307
+ applyFontFamily() {
26308
+ if (this.fontFamilySelect) {
26309
+ this.applyFormatting({ fontFamily: this.fontFamilySelect.value });
26310
+ }
26311
+ }
26312
+ applyFontSize() {
26313
+ if (this.fontSizeSelect) {
26314
+ this.applyFormatting({ fontSize: parseInt(this.fontSizeSelect.value, 10) });
26315
+ }
26316
+ }
26317
+ applyTextColor() {
26318
+ if (this.colorInput) {
26319
+ this.applyFormatting({ color: this.colorInput.value });
26320
+ }
26321
+ }
26322
+ applyHighlight() {
26323
+ if (this.highlightInput) {
26324
+ this.applyFormatting({ backgroundColor: this.highlightInput.value });
26325
+ }
26326
+ }
26327
+ clearHighlight() {
26328
+ this.applyFormatting({ backgroundColor: undefined });
26329
+ }
26330
+ setAlignment(alignment) {
26331
+ if (!this.editor)
26332
+ return;
26333
+ try {
26334
+ this.editor.setUnifiedAlignment(alignment);
26335
+ this.updateAlignmentButtons(alignment);
26336
+ }
26337
+ catch (error) {
26338
+ console.error('Alignment error:', error);
26339
+ }
26340
+ }
26341
+ toggleBulletList() {
26342
+ if (!this.editor)
26343
+ return;
26344
+ try {
26345
+ this.editor.toggleBulletList();
26346
+ this.updateListButtons();
26347
+ }
26348
+ catch (error) {
26349
+ console.error('Bullet list error:', error);
26350
+ }
26351
+ }
26352
+ toggleNumberedList() {
26353
+ if (!this.editor)
26354
+ return;
26355
+ try {
26356
+ this.editor.toggleNumberedList();
26357
+ this.updateListButtons();
26358
+ }
26359
+ catch (error) {
26360
+ console.error('Numbered list error:', error);
26361
+ }
26362
+ }
26363
+ indent() {
26364
+ if (!this.editor)
26365
+ return;
26366
+ try {
26367
+ this.editor.indentParagraph();
26368
+ this.updateListButtons();
26369
+ }
26370
+ catch (error) {
26371
+ console.error('Indent error:', error);
26372
+ }
26373
+ }
26374
+ outdent() {
26375
+ if (!this.editor)
26376
+ return;
26377
+ try {
26378
+ this.editor.outdentParagraph();
26379
+ this.updateListButtons();
26380
+ }
26381
+ catch (error) {
26382
+ console.error('Outdent error:', error);
26383
+ }
26384
+ }
26385
+ /**
26386
+ * Update the pane from current editor state.
26387
+ */
26388
+ update() {
26389
+ this.updateFromEditor();
26390
+ }
26391
+ }
26392
+
26393
+ /**
26394
+ * HyperlinkPane - Edit hyperlink URL and title.
26395
+ *
26396
+ * This pane is shown when a hyperlink is selected/cursor is in a hyperlink.
26397
+ *
26398
+ * Uses the PCEditor public API:
26399
+ * - editor.getHyperlinkAt()
26400
+ * - editor.updateHyperlink()
26401
+ * - editor.removeHyperlink()
26402
+ * - editor.getCursorPosition()
26403
+ */
26404
+ class HyperlinkPane extends BasePane {
26405
+ constructor(id = 'hyperlink', options = {}) {
26406
+ super(id, { className: 'pc-pane-hyperlink', ...options });
26407
+ this.urlInput = null;
26408
+ this.titleInput = null;
26409
+ this.rangeHint = null;
26410
+ this.currentHyperlink = null;
26411
+ this.onApply = options.onApply;
26412
+ this.onRemove = options.onRemove;
26413
+ }
26414
+ attach(options) {
26415
+ super.attach(options);
26416
+ if (this.editor) {
26417
+ // Update on cursor changes
26418
+ const updateHandler = () => this.updateFromCursor();
26419
+ this.editor.on('cursor-changed', updateHandler);
26420
+ this.editor.on('selection-changed', updateHandler);
26421
+ this.eventCleanup.push(() => {
26422
+ this.editor?.off('cursor-changed', updateHandler);
26423
+ this.editor?.off('selection-changed', updateHandler);
26424
+ });
26425
+ // Initial update
26426
+ this.updateFromCursor();
26427
+ }
26428
+ }
26429
+ createContent() {
26430
+ const container = document.createElement('div');
26431
+ // URL input
26432
+ this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26433
+ container.appendChild(this.createFormGroup('URL', this.urlInput));
26434
+ // Title input
26435
+ this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26436
+ container.appendChild(this.createFormGroup('Title', this.titleInput));
26437
+ // Apply button
26438
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26439
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26440
+ container.appendChild(applyBtn);
26441
+ // Remove button
26442
+ const removeBtn = this.createButton('Remove Link', { variant: 'danger' });
26443
+ removeBtn.style.marginTop = '0.5rem';
26444
+ this.addButtonListener(removeBtn, () => this.removeHyperlink());
26445
+ container.appendChild(removeBtn);
26446
+ // Range hint
26447
+ this.rangeHint = this.createHint('');
26448
+ container.appendChild(this.rangeHint);
26449
+ return container;
26450
+ }
26451
+ updateFromCursor() {
26452
+ if (!this.editor)
26453
+ return;
26454
+ const cursorPos = this.editor.getCursorPosition();
26455
+ const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26456
+ if (hyperlink) {
26457
+ this.showHyperlink(hyperlink);
26458
+ }
26459
+ else {
26460
+ this.hideHyperlink();
26461
+ }
26462
+ }
26463
+ showHyperlink(hyperlink) {
26464
+ this.currentHyperlink = hyperlink;
26465
+ if (this.urlInput) {
26466
+ this.urlInput.value = hyperlink.url;
26467
+ }
26468
+ if (this.titleInput) {
26469
+ this.titleInput.value = hyperlink.title || '';
26470
+ }
26471
+ if (this.rangeHint) {
26472
+ this.rangeHint.textContent = `Link spans characters ${hyperlink.startIndex} to ${hyperlink.endIndex}`;
26473
+ }
26474
+ // Show the pane
26475
+ this.show();
26476
+ }
26477
+ hideHyperlink() {
26478
+ this.currentHyperlink = null;
26479
+ this.hide();
26480
+ }
26481
+ applyChanges() {
26482
+ if (!this.editor || !this.currentHyperlink)
26483
+ return;
26484
+ try {
26485
+ const url = this.urlInput?.value.trim() || '';
26486
+ const title = this.titleInput?.value.trim() || undefined;
26487
+ if (!url) {
26488
+ this.onApply?.(false, new Error('URL is required'));
26489
+ return;
26490
+ }
26491
+ this.editor.updateHyperlink(this.currentHyperlink.id, { url, title });
26492
+ // Update local reference
26493
+ this.currentHyperlink.url = url;
26494
+ this.currentHyperlink.title = title;
26495
+ this.onApply?.(true);
26496
+ }
26497
+ catch (error) {
26498
+ this.onApply?.(false, error instanceof Error ? error : new Error(String(error)));
26499
+ }
26500
+ }
26501
+ removeHyperlink() {
26502
+ if (!this.editor || !this.currentHyperlink)
26503
+ return;
26504
+ try {
26505
+ this.editor.removeHyperlink(this.currentHyperlink.id);
26506
+ this.hideHyperlink();
26507
+ this.onRemove?.(true);
26508
+ }
26509
+ catch {
26510
+ this.onRemove?.(false);
26511
+ }
26512
+ }
26513
+ /**
26514
+ * Get the currently selected hyperlink.
26515
+ */
26516
+ getCurrentHyperlink() {
26517
+ return this.currentHyperlink;
26518
+ }
26519
+ /**
26520
+ * Check if a hyperlink is currently selected.
26521
+ */
26522
+ hasHyperlink() {
26523
+ return this.currentHyperlink !== null;
26524
+ }
26525
+ /**
26526
+ * Update the pane from current editor state.
26527
+ */
26528
+ update() {
26529
+ this.updateFromCursor();
26530
+ }
26531
+ }
26532
+
26533
+ /**
26534
+ * SubstitutionFieldPane - Edit substitution field properties.
26535
+ *
26536
+ * Shows:
26537
+ * - Field name
26538
+ * - Default value
26539
+ * - Format configuration (value type, number/currency/date formats)
26540
+ *
26541
+ * Uses the PCEditor public API:
26542
+ * - editor.getFieldAt()
26543
+ * - editor.updateField()
26544
+ */
26545
+ class SubstitutionFieldPane extends BasePane {
26546
+ constructor(id = 'substitution-field', options = {}) {
26547
+ super(id, { className: 'pc-pane-substitution-field', ...options });
26548
+ this.fieldNameInput = null;
26549
+ this.fieldDefaultInput = null;
26550
+ this.valueTypeSelect = null;
26551
+ this.numberFormatSelect = null;
26552
+ this.currencyFormatSelect = null;
26553
+ this.dateFormatSelect = null;
26554
+ this.positionHint = null;
26555
+ this.numberFormatGroup = null;
26556
+ this.currencyFormatGroup = null;
26557
+ this.dateFormatGroup = null;
26558
+ this.currentField = null;
26559
+ this.onApplyCallback = options.onApply;
26560
+ }
26561
+ attach(options) {
26562
+ super.attach(options);
26563
+ if (this.editor) {
26564
+ // Listen for field selection events
26565
+ const selectionHandler = (event) => {
26566
+ if (event.type === 'field' && event.field) {
26567
+ this.showField(event.field);
26568
+ }
26569
+ else if (!event.type || event.type !== 'field') ;
26570
+ };
26571
+ this.editor.on('selection-change', selectionHandler);
26572
+ this.eventCleanup.push(() => {
26573
+ this.editor?.off('selection-change', selectionHandler);
26574
+ });
26575
+ }
26576
+ }
26577
+ createContent() {
26578
+ const container = document.createElement('div');
26579
+ // Field name input
26580
+ this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26581
+ container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
26582
+ // Default value input
26583
+ this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26584
+ container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
26585
+ // Value type select
26586
+ this.valueTypeSelect = this.createSelect([
26587
+ { value: '', label: '(None)' },
26588
+ { value: 'number', label: 'Number' },
26589
+ { value: 'currency', label: 'Currency' },
26590
+ { value: 'date', label: 'Date' }
26591
+ ]);
26592
+ this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26593
+ container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
26594
+ // Number format group
26595
+ this.numberFormatGroup = this.createSection();
26596
+ this.numberFormatGroup.style.display = 'none';
26597
+ this.numberFormatSelect = this.createSelect([
26598
+ { value: '0', label: 'Integer (0)' },
26599
+ { value: '0.00', label: 'Two decimals (0.00)' },
26600
+ { value: '0,0', label: 'Thousands separator (0,0)' },
26601
+ { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26602
+ ]);
26603
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
26604
+ container.appendChild(this.numberFormatGroup);
26605
+ // Currency format group
26606
+ this.currencyFormatGroup = this.createSection();
26607
+ this.currencyFormatGroup.style.display = 'none';
26608
+ this.currencyFormatSelect = this.createSelect([
26609
+ { value: 'USD', label: 'USD ($)' },
26610
+ { value: 'EUR', label: 'EUR' },
26611
+ { value: 'GBP', label: 'GBP' },
26612
+ { value: 'JPY', label: 'JPY' }
26613
+ ]);
26614
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
26615
+ container.appendChild(this.currencyFormatGroup);
26616
+ // Date format group
26617
+ this.dateFormatGroup = this.createSection();
26618
+ this.dateFormatGroup.style.display = 'none';
26619
+ this.dateFormatSelect = this.createSelect([
26620
+ { value: 'MMMM D, YYYY', label: 'January 1, 2026' },
26621
+ { value: 'MM/DD/YYYY', label: '01/01/2026' },
26622
+ { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26623
+ { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26624
+ ]);
26625
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
26626
+ container.appendChild(this.dateFormatGroup);
26627
+ // Apply button
26628
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26629
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26630
+ container.appendChild(applyBtn);
26631
+ // Position hint
26632
+ this.positionHint = this.createHint('');
26633
+ container.appendChild(this.positionHint);
26634
+ return container;
26635
+ }
26636
+ updateFormatGroups() {
26637
+ const valueType = this.valueTypeSelect?.value || '';
26638
+ if (this.numberFormatGroup) {
26639
+ this.numberFormatGroup.style.display = valueType === 'number' ? 'block' : 'none';
26640
+ }
26641
+ if (this.currencyFormatGroup) {
26642
+ this.currencyFormatGroup.style.display = valueType === 'currency' ? 'block' : 'none';
26643
+ }
26644
+ if (this.dateFormatGroup) {
26645
+ this.dateFormatGroup.style.display = valueType === 'date' ? 'block' : 'none';
26646
+ }
26647
+ }
26648
+ /**
26649
+ * Show the pane with the given field.
26650
+ */
26651
+ showField(field) {
26652
+ this.currentField = field;
26653
+ if (this.fieldNameInput) {
26654
+ this.fieldNameInput.value = field.fieldName;
26655
+ }
26656
+ if (this.fieldDefaultInput) {
26657
+ this.fieldDefaultInput.value = field.defaultValue || '';
26658
+ }
26659
+ if (this.positionHint) {
26660
+ this.positionHint.textContent = `Field at position ${field.textIndex}`;
26661
+ }
26662
+ // Populate format options
26663
+ if (this.valueTypeSelect) {
26664
+ this.valueTypeSelect.value = field.formatConfig?.valueType || '';
26665
+ }
26666
+ if (this.numberFormatSelect && field.formatConfig?.numberFormat) {
26667
+ this.numberFormatSelect.value = field.formatConfig.numberFormat;
26668
+ }
26669
+ if (this.currencyFormatSelect && field.formatConfig?.currencyFormat) {
26670
+ this.currencyFormatSelect.value = field.formatConfig.currencyFormat;
26671
+ }
26672
+ if (this.dateFormatSelect && field.formatConfig?.dateFormat) {
26673
+ this.dateFormatSelect.value = field.formatConfig.dateFormat;
26674
+ }
26675
+ this.updateFormatGroups();
26676
+ this.show();
26677
+ }
26678
+ /**
26679
+ * Hide the pane and clear the current field.
26680
+ */
26681
+ hideField() {
26682
+ this.currentField = null;
26683
+ this.hide();
26684
+ }
26685
+ applyChanges() {
26686
+ if (!this.editor || !this.currentField) {
26687
+ this.onApplyCallback?.(false, new Error('No field selected'));
26688
+ return;
26689
+ }
26690
+ const fieldName = this.fieldNameInput?.value.trim();
26691
+ if (!fieldName) {
26692
+ this.onApplyCallback?.(false, new Error('Field name cannot be empty'));
26693
+ return;
26694
+ }
26695
+ const updates = {};
26696
+ if (fieldName !== this.currentField.fieldName) {
26697
+ updates.fieldName = fieldName;
26698
+ }
26699
+ const defaultValue = this.fieldDefaultInput?.value || undefined;
26700
+ if (defaultValue !== this.currentField.defaultValue) {
26701
+ updates.defaultValue = defaultValue;
26702
+ }
26703
+ // Build format config
26704
+ const valueType = this.valueTypeSelect?.value;
26705
+ if (valueType) {
26706
+ const formatConfig = {
26707
+ valueType: valueType
26708
+ };
26709
+ if (valueType === 'number' && this.numberFormatSelect?.value) {
26710
+ formatConfig.numberFormat = this.numberFormatSelect.value;
26711
+ }
26712
+ else if (valueType === 'currency' && this.currencyFormatSelect?.value) {
26713
+ formatConfig.currencyFormat = this.currencyFormatSelect.value;
26714
+ }
26715
+ else if (valueType === 'date' && this.dateFormatSelect?.value) {
26716
+ formatConfig.dateFormat = this.dateFormatSelect.value;
26717
+ }
26718
+ updates.formatConfig = formatConfig;
26719
+ }
26720
+ else if (this.currentField.formatConfig) {
26721
+ updates.formatConfig = undefined;
26722
+ }
26723
+ if (Object.keys(updates).length === 0) {
26724
+ return; // No changes
26725
+ }
26726
+ try {
26727
+ const success = this.editor.updateField(this.currentField.textIndex, updates);
26728
+ if (success) {
26729
+ // Update the current field reference
26730
+ this.currentField = this.editor.getFieldAt(this.currentField.textIndex) || null;
26731
+ this.onApplyCallback?.(true);
26732
+ }
26733
+ else {
26734
+ this.onApplyCallback?.(false, new Error('Failed to update field'));
26735
+ }
26736
+ }
26737
+ catch (error) {
26738
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
26739
+ }
26740
+ }
26741
+ /**
26742
+ * Get the currently selected field.
26743
+ */
26744
+ getCurrentField() {
26745
+ return this.currentField;
26746
+ }
26747
+ /**
26748
+ * Check if a field is currently selected.
26749
+ */
26750
+ hasField() {
26751
+ return this.currentField !== null;
26752
+ }
26753
+ /**
26754
+ * Update the pane from current editor state.
26755
+ */
26756
+ update() {
26757
+ // Field pane doesn't auto-update - it's driven by selection events
26758
+ }
26759
+ }
26760
+
26761
+ /**
26762
+ * RepeatingSectionPane - Edit repeating section (loop) properties.
26763
+ *
26764
+ * Shows:
26765
+ * - Field path (array property in merge data)
26766
+ * - Position information
26767
+ *
26768
+ * Uses the PCEditor public API:
26769
+ * - editor.getRepeatingSection()
26770
+ * - editor.updateRepeatingSectionFieldPath()
26771
+ * - editor.removeRepeatingSection()
26772
+ */
26773
+ class RepeatingSectionPane extends BasePane {
26774
+ constructor(id = 'repeating-section', options = {}) {
26775
+ super(id, { className: 'pc-pane-repeating-section', ...options });
26776
+ this.fieldPathInput = null;
26777
+ this.positionHint = null;
26778
+ this.currentSection = null;
26779
+ this.onApplyCallback = options.onApply;
26780
+ this.onRemoveCallback = options.onRemove;
26781
+ }
26782
+ attach(options) {
26783
+ super.attach(options);
26784
+ if (this.editor) {
26785
+ // Listen for repeating section selection
26786
+ const selectionHandler = (event) => {
26787
+ if (event.type === 'repeating-section' && event.sectionId) {
26788
+ const section = this.editor?.getRepeatingSection(event.sectionId);
26789
+ if (section) {
26790
+ this.showSection(section);
26791
+ }
26792
+ }
26793
+ };
26794
+ const removedHandler = () => {
26795
+ this.hideSection();
26796
+ };
26797
+ this.editor.on('selection-change', selectionHandler);
26798
+ this.editor.on('repeating-section-removed', removedHandler);
26799
+ this.eventCleanup.push(() => {
26800
+ this.editor?.off('selection-change', selectionHandler);
26801
+ this.editor?.off('repeating-section-removed', removedHandler);
26802
+ });
26803
+ }
26804
+ }
26805
+ createContent() {
26806
+ const container = document.createElement('div');
26807
+ // Field path input
26808
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
26809
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
26810
+ hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
26811
+ }));
26812
+ // Apply button
26813
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26814
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26815
+ container.appendChild(applyBtn);
26816
+ // Remove button
26817
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
26818
+ removeBtn.style.marginTop = '0.5rem';
26819
+ this.addButtonListener(removeBtn, () => this.removeSection());
26820
+ container.appendChild(removeBtn);
26821
+ // Position hint
26822
+ this.positionHint = this.createHint('');
26823
+ container.appendChild(this.positionHint);
26824
+ return container;
26825
+ }
26826
+ /**
26827
+ * Show the pane with the given section.
26828
+ */
26829
+ showSection(section) {
26830
+ this.currentSection = section;
26831
+ if (this.fieldPathInput) {
26832
+ this.fieldPathInput.value = section.fieldPath;
26833
+ }
26834
+ if (this.positionHint) {
26835
+ this.positionHint.textContent = `Loop from position ${section.startIndex} to ${section.endIndex}`;
26836
+ }
26837
+ this.show();
26838
+ }
26839
+ /**
26840
+ * Hide the pane and clear the current section.
26841
+ */
26842
+ hideSection() {
26843
+ this.currentSection = null;
26844
+ this.hide();
26845
+ }
26846
+ applyChanges() {
26847
+ if (!this.editor || !this.currentSection) {
26848
+ this.onApplyCallback?.(false, new Error('No section selected'));
26849
+ return;
26850
+ }
26851
+ const fieldPath = this.fieldPathInput?.value.trim();
26852
+ if (!fieldPath) {
26853
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
26854
+ return;
26855
+ }
26856
+ if (fieldPath === this.currentSection.fieldPath) {
26857
+ return; // No changes
26858
+ }
26859
+ try {
26860
+ const success = this.editor.updateRepeatingSectionFieldPath(this.currentSection.id, fieldPath);
26861
+ if (success) {
26862
+ // Update the current section reference
26863
+ this.currentSection = this.editor.getRepeatingSection(this.currentSection.id) || null;
26864
+ if (this.currentSection) {
26865
+ this.showSection(this.currentSection);
26866
+ }
26867
+ this.onApplyCallback?.(true);
26868
+ }
26869
+ else {
26870
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
26871
+ }
26872
+ }
26873
+ catch (error) {
26874
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
26875
+ }
26876
+ }
26877
+ removeSection() {
26878
+ if (!this.editor || !this.currentSection)
26879
+ return;
26880
+ try {
26881
+ this.editor.removeRepeatingSection(this.currentSection.id);
26882
+ this.hideSection();
26883
+ this.onRemoveCallback?.(true);
26884
+ }
26885
+ catch {
26886
+ this.onRemoveCallback?.(false);
26887
+ }
26888
+ }
26889
+ /**
26890
+ * Get the currently selected section.
26891
+ */
26892
+ getCurrentSection() {
26893
+ return this.currentSection;
26894
+ }
26895
+ /**
26896
+ * Check if a section is currently selected.
26897
+ */
26898
+ hasSection() {
26899
+ return this.currentSection !== null;
26900
+ }
26901
+ /**
26902
+ * Update the pane from current editor state.
26903
+ */
26904
+ update() {
26905
+ // Section pane doesn't auto-update - it's driven by selection events
26906
+ }
26907
+ }
26908
+
26909
+ /**
26910
+ * TableRowLoopPane - Edit table row loop properties.
26911
+ *
26912
+ * Shows:
26913
+ * - Field path (array property in merge data)
26914
+ * - Row range information
26915
+ *
26916
+ * Uses the TableObject API:
26917
+ * - table.getRowLoop()
26918
+ * - table.updateRowLoopFieldPath()
26919
+ * - table.removeRowLoop()
26920
+ */
26921
+ class TableRowLoopPane extends BasePane {
26922
+ constructor(id = 'table-row-loop', options = {}) {
26923
+ super(id, { className: 'pc-pane-table-row-loop', ...options });
26924
+ this.fieldPathInput = null;
26925
+ this.rangeHint = null;
26926
+ this.currentLoop = null;
26927
+ this.currentTable = null;
26928
+ this.onApplyCallback = options.onApply;
26929
+ this.onRemoveCallback = options.onRemove;
26930
+ }
26931
+ attach(options) {
26932
+ super.attach(options);
26933
+ // Table row loop pane is typically shown manually when a table's row loop is selected
26934
+ // The consumer is responsible for calling showLoop() with the table and loop
26935
+ }
26936
+ createContent() {
26937
+ const container = document.createElement('div');
26938
+ // Field path input
26939
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
26940
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
26941
+ hint: 'Path to array in merge data (e.g., "items" or "orders")'
26942
+ }));
26943
+ // Apply button
26944
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26945
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26946
+ container.appendChild(applyBtn);
26947
+ // Remove button
26948
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
26949
+ removeBtn.style.marginTop = '0.5rem';
26950
+ this.addButtonListener(removeBtn, () => this.removeLoop());
26951
+ container.appendChild(removeBtn);
26952
+ // Range hint
26953
+ this.rangeHint = this.createHint('');
26954
+ container.appendChild(this.rangeHint);
26955
+ return container;
26956
+ }
26957
+ /**
26958
+ * Show the pane with the given table and loop.
26959
+ */
26960
+ showLoop(table, loop) {
26961
+ this.currentTable = table;
26962
+ this.currentLoop = loop;
26963
+ if (this.fieldPathInput) {
26964
+ this.fieldPathInput.value = loop.fieldPath;
26965
+ }
26966
+ if (this.rangeHint) {
26967
+ this.rangeHint.textContent = `Rows ${loop.startRowIndex} - ${loop.endRowIndex}`;
26968
+ }
26969
+ this.show();
26970
+ }
26971
+ /**
26972
+ * Hide the pane and clear current loop.
26973
+ */
26974
+ hideLoop() {
26975
+ this.currentTable = null;
26976
+ this.currentLoop = null;
26977
+ this.hide();
26978
+ }
26979
+ applyChanges() {
26980
+ if (!this.currentTable || !this.currentLoop) {
26981
+ this.onApplyCallback?.(false, new Error('No loop selected'));
26982
+ return;
26983
+ }
26984
+ const fieldPath = this.fieldPathInput?.value.trim();
26985
+ if (!fieldPath) {
26986
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
26987
+ return;
26988
+ }
26989
+ if (fieldPath === this.currentLoop.fieldPath) {
26990
+ return; // No changes
26991
+ }
26992
+ try {
26993
+ const success = this.currentTable.updateRowLoopFieldPath(this.currentLoop.id, fieldPath);
26994
+ if (success) {
26995
+ // Update the current loop reference
26996
+ this.currentLoop = this.currentTable.getRowLoop(this.currentLoop.id) || null;
26997
+ if (this.currentLoop) {
26998
+ this.showLoop(this.currentTable, this.currentLoop);
26999
+ }
27000
+ this.onApplyCallback?.(true);
27001
+ }
27002
+ else {
27003
+ this.onApplyCallback?.(false, new Error('Failed to update loop'));
27004
+ }
27005
+ }
27006
+ catch (error) {
27007
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27008
+ }
27009
+ }
27010
+ removeLoop() {
27011
+ if (!this.currentTable || !this.currentLoop)
27012
+ return;
27013
+ try {
27014
+ const success = this.currentTable.removeRowLoop(this.currentLoop.id);
27015
+ if (success) {
27016
+ this.hideLoop();
27017
+ this.onRemoveCallback?.(true);
27018
+ }
27019
+ else {
27020
+ this.onRemoveCallback?.(false);
27021
+ }
27022
+ }
27023
+ catch {
27024
+ this.onRemoveCallback?.(false);
27025
+ }
27026
+ }
27027
+ /**
27028
+ * Get the currently selected loop.
27029
+ */
27030
+ getCurrentLoop() {
27031
+ return this.currentLoop;
27032
+ }
27033
+ /**
27034
+ * Get the currently selected table.
27035
+ */
27036
+ getCurrentTable() {
27037
+ return this.currentTable;
27038
+ }
27039
+ /**
27040
+ * Check if a loop is currently selected.
27041
+ */
27042
+ hasLoop() {
27043
+ return this.currentLoop !== null;
27044
+ }
27045
+ /**
27046
+ * Update the pane from current editor state.
27047
+ */
27048
+ update() {
27049
+ // Table row loop pane doesn't auto-update - it's driven by showLoop() calls
27050
+ }
27051
+ }
27052
+
27053
+ /**
27054
+ * TextBoxPane - Edit text box properties.
27055
+ *
27056
+ * Shows:
27057
+ * - Position (inline, block, relative)
27058
+ * - Relative offset (for relative positioning)
27059
+ * - Background color
27060
+ * - Border (width, color, style)
27061
+ * - Padding
27062
+ *
27063
+ * Uses the PCEditor public API:
27064
+ * - editor.getSelectedTextBox()
27065
+ * - editor.updateTextBox()
27066
+ */
27067
+ class TextBoxPane extends BasePane {
27068
+ constructor(id = 'textbox', options = {}) {
27069
+ super(id, { className: 'pc-pane-textbox', ...options });
27070
+ this.positionSelect = null;
27071
+ this.offsetGroup = null;
27072
+ this.offsetXInput = null;
27073
+ this.offsetYInput = null;
27074
+ this.bgColorInput = null;
27075
+ this.borderWidthInput = null;
27076
+ this.borderColorInput = null;
27077
+ this.borderStyleSelect = null;
27078
+ this.paddingInput = null;
27079
+ this.currentTextBox = null;
27080
+ this.onApplyCallback = options.onApply;
27081
+ }
27082
+ attach(options) {
27083
+ super.attach(options);
27084
+ if (this.editor) {
27085
+ // Listen for selection changes
27086
+ const updateHandler = () => this.updateFromSelection();
27087
+ this.editor.on('selection-change', updateHandler);
27088
+ this.editor.on('textbox-updated', updateHandler);
27089
+ this.eventCleanup.push(() => {
27090
+ this.editor?.off('selection-change', updateHandler);
27091
+ this.editor?.off('textbox-updated', updateHandler);
27092
+ });
27093
+ // Initial update
27094
+ this.updateFromSelection();
27095
+ }
27096
+ }
27097
+ createContent() {
27098
+ const container = document.createElement('div');
27099
+ // Position section
27100
+ const positionSection = this.createSection('Position');
27101
+ this.positionSelect = this.createSelect([
27102
+ { value: 'inline', label: 'Inline' },
27103
+ { value: 'block', label: 'Block' },
27104
+ { value: 'relative', label: 'Relative' }
27105
+ ], 'inline');
27106
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27107
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27108
+ // Offset group (only visible for relative positioning)
27109
+ this.offsetGroup = document.createElement('div');
27110
+ this.offsetGroup.style.display = 'none';
27111
+ const offsetRow = this.createRow();
27112
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27113
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27114
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27115
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27116
+ this.offsetGroup.appendChild(offsetRow);
27117
+ positionSection.appendChild(this.offsetGroup);
27118
+ container.appendChild(positionSection);
27119
+ // Background section
27120
+ const bgSection = this.createSection('Background');
27121
+ this.bgColorInput = this.createColorInput('#ffffff');
27122
+ bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
27123
+ container.appendChild(bgSection);
27124
+ // Border section
27125
+ const borderSection = this.createSection('Border');
27126
+ const borderRow = this.createRow();
27127
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27128
+ this.borderColorInput = this.createColorInput('#cccccc');
27129
+ borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27130
+ borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27131
+ borderSection.appendChild(borderRow);
27132
+ this.borderStyleSelect = this.createSelect([
27133
+ { value: 'solid', label: 'Solid' },
27134
+ { value: 'dashed', label: 'Dashed' },
27135
+ { value: 'dotted', label: 'Dotted' },
27136
+ { value: 'none', label: 'None' }
27137
+ ], 'solid');
27138
+ borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27139
+ container.appendChild(borderSection);
27140
+ // Padding section
27141
+ const paddingSection = this.createSection('Padding');
27142
+ this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27143
+ paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
27144
+ container.appendChild(paddingSection);
27145
+ // Apply button
27146
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27147
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27148
+ container.appendChild(applyBtn);
27149
+ return container;
27150
+ }
27151
+ updateFromSelection() {
27152
+ if (!this.editor)
27153
+ return;
27154
+ const textBox = this.editor.getSelectedTextBox?.();
27155
+ if (textBox && !textBox.editing) {
27156
+ this.showTextBox(textBox);
27157
+ }
27158
+ else {
27159
+ this.hideTextBox();
27160
+ }
27161
+ }
27162
+ /**
27163
+ * Show the pane with the given text box.
27164
+ */
27165
+ showTextBox(textBox) {
27166
+ this.currentTextBox = textBox;
27167
+ // Populate position
27168
+ if (this.positionSelect) {
27169
+ this.positionSelect.value = textBox.position || 'inline';
27170
+ }
27171
+ this.updateOffsetVisibility();
27172
+ // Populate offset
27173
+ if (this.offsetXInput) {
27174
+ this.offsetXInput.value = String(textBox.relativeOffset?.x ?? 0);
27175
+ }
27176
+ if (this.offsetYInput) {
27177
+ this.offsetYInput.value = String(textBox.relativeOffset?.y ?? 0);
27178
+ }
27179
+ // Populate background
27180
+ if (this.bgColorInput) {
27181
+ this.bgColorInput.value = textBox.backgroundColor || '#ffffff';
27182
+ }
27183
+ // Populate border (use first side with non-none style)
27184
+ const border = textBox.border;
27185
+ const activeBorder = border.top.style !== 'none' ? border.top :
27186
+ border.right.style !== 'none' ? border.right :
27187
+ border.bottom.style !== 'none' ? border.bottom :
27188
+ border.left.style !== 'none' ? border.left : border.top;
27189
+ if (this.borderWidthInput) {
27190
+ this.borderWidthInput.value = String(activeBorder.width);
27191
+ }
27192
+ if (this.borderColorInput) {
27193
+ this.borderColorInput.value = activeBorder.color;
27194
+ }
27195
+ if (this.borderStyleSelect) {
27196
+ this.borderStyleSelect.value = activeBorder.style;
27197
+ }
27198
+ // Populate padding
27199
+ if (this.paddingInput) {
27200
+ this.paddingInput.value = String(textBox.padding ?? 8);
27201
+ }
27202
+ this.show();
27203
+ }
27204
+ /**
27205
+ * Hide the pane and clear current text box.
27206
+ */
27207
+ hideTextBox() {
27208
+ this.currentTextBox = null;
27209
+ this.hide();
27210
+ }
27211
+ updateOffsetVisibility() {
27212
+ if (this.offsetGroup && this.positionSelect) {
27213
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27214
+ }
27215
+ }
27216
+ applyChanges() {
27217
+ if (!this.editor || !this.currentTextBox) {
27218
+ this.onApplyCallback?.(false, new Error('No text box selected'));
27219
+ return;
27220
+ }
27221
+ const updates = {};
27222
+ // Position
27223
+ if (this.positionSelect) {
27224
+ updates.position = this.positionSelect.value;
27225
+ }
27226
+ // Relative offset
27227
+ if (this.positionSelect?.value === 'relative') {
27228
+ updates.relativeOffset = {
27229
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27230
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27231
+ };
27232
+ }
27233
+ // Background color
27234
+ if (this.bgColorInput) {
27235
+ updates.backgroundColor = this.bgColorInput.value;
27236
+ }
27237
+ // Border
27238
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27239
+ const color = this.borderColorInput?.value || '#cccccc';
27240
+ const style = (this.borderStyleSelect?.value || 'solid');
27241
+ const borderSide = { width, color, style };
27242
+ updates.border = {
27243
+ top: { ...borderSide },
27244
+ right: { ...borderSide },
27245
+ bottom: { ...borderSide },
27246
+ left: { ...borderSide }
27247
+ };
27248
+ // Padding
27249
+ if (this.paddingInput) {
27250
+ updates.padding = parseInt(this.paddingInput.value, 10);
27251
+ }
27252
+ try {
27253
+ const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27254
+ if (success) {
27255
+ this.onApplyCallback?.(true);
27256
+ }
27257
+ else {
27258
+ this.onApplyCallback?.(false, new Error('Failed to update text box'));
27259
+ }
27260
+ }
27261
+ catch (error) {
27262
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27263
+ }
27264
+ }
27265
+ /**
27266
+ * Get the currently selected text box.
27267
+ */
27268
+ getCurrentTextBox() {
27269
+ return this.currentTextBox;
27270
+ }
27271
+ /**
27272
+ * Check if a text box is currently selected.
27273
+ */
27274
+ hasTextBox() {
27275
+ return this.currentTextBox !== null;
27276
+ }
27277
+ /**
27278
+ * Update the pane from current editor state.
27279
+ */
27280
+ update() {
27281
+ this.updateFromSelection();
27282
+ }
27283
+ }
27284
+
27285
+ /**
27286
+ * ImagePane - Edit image properties.
27287
+ *
27288
+ * Shows:
27289
+ * - Position (inline, block, relative)
27290
+ * - Relative offset (for relative positioning)
27291
+ * - Fit mode (contain, cover, fill, none, tile)
27292
+ * - Resize mode (free, locked-aspect-ratio)
27293
+ * - Alt text
27294
+ * - Source file picker
27295
+ *
27296
+ * Uses the PCEditor public API:
27297
+ * - editor.getSelectedImage()
27298
+ * - editor.updateImage()
27299
+ * - editor.setImageSource()
27300
+ */
27301
+ class ImagePane extends BasePane {
27302
+ constructor(id = 'image', options = {}) {
27303
+ super(id, { className: 'pc-pane-image', ...options });
27304
+ this.positionSelect = null;
27305
+ this.offsetGroup = null;
27306
+ this.offsetXInput = null;
27307
+ this.offsetYInput = null;
27308
+ this.fitModeSelect = null;
27309
+ this.resizeModeSelect = null;
27310
+ this.altTextInput = null;
27311
+ this.fileInput = null;
27312
+ this.currentImage = null;
27313
+ this.maxImageWidth = options.maxImageWidth ?? 400;
27314
+ this.maxImageHeight = options.maxImageHeight ?? 400;
27315
+ this.onApplyCallback = options.onApply;
27316
+ }
27317
+ attach(options) {
27318
+ super.attach(options);
27319
+ if (this.editor) {
27320
+ // Listen for selection changes
27321
+ const updateHandler = () => this.updateFromSelection();
27322
+ this.editor.on('selection-change', updateHandler);
27323
+ this.editor.on('image-updated', updateHandler);
27324
+ this.eventCleanup.push(() => {
27325
+ this.editor?.off('selection-change', updateHandler);
27326
+ this.editor?.off('image-updated', updateHandler);
27327
+ });
27328
+ // Initial update
27329
+ this.updateFromSelection();
27330
+ }
27331
+ }
27332
+ createContent() {
27333
+ const container = document.createElement('div');
27334
+ // Position section
27335
+ const positionSection = this.createSection('Position');
27336
+ this.positionSelect = this.createSelect([
27337
+ { value: 'inline', label: 'Inline' },
27338
+ { value: 'block', label: 'Block' },
27339
+ { value: 'relative', label: 'Relative' }
27340
+ ], 'inline');
27341
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27342
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27343
+ // Offset group (only visible for relative positioning)
27344
+ this.offsetGroup = document.createElement('div');
27345
+ this.offsetGroup.style.display = 'none';
27346
+ const offsetRow = this.createRow();
27347
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27348
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27349
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27350
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27351
+ this.offsetGroup.appendChild(offsetRow);
27352
+ positionSection.appendChild(this.offsetGroup);
27353
+ container.appendChild(positionSection);
27354
+ // Fit mode section
27355
+ const fitSection = this.createSection('Display');
27356
+ this.fitModeSelect = this.createSelect([
27357
+ { value: 'contain', label: 'Contain' },
27358
+ { value: 'cover', label: 'Cover' },
27359
+ { value: 'fill', label: 'Fill' },
27360
+ { value: 'none', label: 'None (original size)' },
27361
+ { value: 'tile', label: 'Tile' }
27362
+ ], 'contain');
27363
+ fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
27364
+ this.resizeModeSelect = this.createSelect([
27365
+ { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27366
+ { value: 'free', label: 'Free Resize' }
27367
+ ], 'locked-aspect-ratio');
27368
+ fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27369
+ container.appendChild(fitSection);
27370
+ // Alt text section
27371
+ const altSection = this.createSection('Accessibility');
27372
+ this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27373
+ altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27374
+ container.appendChild(altSection);
27375
+ // Source section
27376
+ const sourceSection = this.createSection('Source');
27377
+ this.fileInput = document.createElement('input');
27378
+ this.fileInput.type = 'file';
27379
+ this.fileInput.accept = 'image/*';
27380
+ this.fileInput.style.display = 'none';
27381
+ this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27382
+ sourceSection.appendChild(this.fileInput);
27383
+ const changeSourceBtn = this.createButton('Change Image...');
27384
+ this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27385
+ sourceSection.appendChild(changeSourceBtn);
27386
+ container.appendChild(sourceSection);
27387
+ // Apply button
27388
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27389
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27390
+ container.appendChild(applyBtn);
27391
+ return container;
27392
+ }
27393
+ updateFromSelection() {
27394
+ if (!this.editor)
27395
+ return;
27396
+ const image = this.editor.getSelectedImage?.();
27397
+ if (image) {
27398
+ this.showImage(image);
27399
+ }
27400
+ else {
27401
+ this.hideImage();
27402
+ }
27403
+ }
27404
+ /**
27405
+ * Show the pane with the given image.
27406
+ */
27407
+ showImage(image) {
27408
+ this.currentImage = image;
27409
+ // Populate position
27410
+ if (this.positionSelect) {
27411
+ this.positionSelect.value = image.position || 'inline';
27412
+ }
27413
+ this.updateOffsetVisibility();
27414
+ // Populate offset
27415
+ if (this.offsetXInput) {
27416
+ this.offsetXInput.value = String(image.relativeOffset?.x ?? 0);
27417
+ }
27418
+ if (this.offsetYInput) {
27419
+ this.offsetYInput.value = String(image.relativeOffset?.y ?? 0);
27420
+ }
27421
+ // Populate fit mode
27422
+ if (this.fitModeSelect) {
27423
+ this.fitModeSelect.value = image.fit || 'contain';
27424
+ }
27425
+ // Populate resize mode
27426
+ if (this.resizeModeSelect) {
27427
+ this.resizeModeSelect.value = image.resizeMode || 'locked-aspect-ratio';
27428
+ }
27429
+ // Populate alt text
27430
+ if (this.altTextInput) {
27431
+ this.altTextInput.value = image.alt || '';
27432
+ }
27433
+ this.show();
27434
+ }
27435
+ /**
27436
+ * Hide the pane and clear current image.
27437
+ */
27438
+ hideImage() {
27439
+ this.currentImage = null;
27440
+ this.hide();
27441
+ }
27442
+ updateOffsetVisibility() {
27443
+ if (this.offsetGroup && this.positionSelect) {
27444
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27445
+ }
27446
+ }
27447
+ handleFileChange(event) {
27448
+ if (!this.editor || !this.currentImage)
27449
+ return;
27450
+ const input = event.target;
27451
+ const file = input.files?.[0];
27452
+ if (!file)
27453
+ return;
27454
+ const reader = new FileReader();
27455
+ reader.onload = (e) => {
27456
+ const dataUrl = e.target?.result;
27457
+ if (dataUrl && this.currentImage && this.editor) {
27458
+ this.editor.setImageSource(this.currentImage.id, dataUrl, {
27459
+ maxWidth: this.maxImageWidth,
27460
+ maxHeight: this.maxImageHeight
27461
+ });
27462
+ }
27463
+ };
27464
+ reader.readAsDataURL(file);
27465
+ // Reset file input so the same file can be selected again
27466
+ input.value = '';
27467
+ }
27468
+ applyChanges() {
27469
+ if (!this.editor || !this.currentImage) {
27470
+ this.onApplyCallback?.(false, new Error('No image selected'));
27471
+ return;
27472
+ }
27473
+ const updates = {};
27474
+ // Position
27475
+ if (this.positionSelect) {
27476
+ updates.position = this.positionSelect.value;
27477
+ }
27478
+ // Relative offset
27479
+ if (this.positionSelect?.value === 'relative') {
27480
+ updates.relativeOffset = {
27481
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27482
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27483
+ };
27484
+ }
27485
+ // Fit mode
27486
+ if (this.fitModeSelect) {
27487
+ updates.fit = this.fitModeSelect.value;
27488
+ }
27489
+ // Resize mode
27490
+ if (this.resizeModeSelect) {
27491
+ updates.resizeMode = this.resizeModeSelect.value;
27492
+ }
27493
+ // Alt text
27494
+ if (this.altTextInput) {
27495
+ updates.alt = this.altTextInput.value;
27496
+ }
27497
+ try {
27498
+ const success = this.editor.updateImage(this.currentImage.id, updates);
27499
+ if (success) {
27500
+ this.onApplyCallback?.(true);
27501
+ }
27502
+ else {
27503
+ this.onApplyCallback?.(false, new Error('Failed to update image'));
27504
+ }
27505
+ }
27506
+ catch (error) {
27507
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27508
+ }
27509
+ }
27510
+ /**
27511
+ * Get the currently selected image.
27512
+ */
27513
+ getCurrentImage() {
27514
+ return this.currentImage;
27515
+ }
27516
+ /**
27517
+ * Check if an image is currently selected.
27518
+ */
27519
+ hasImage() {
27520
+ return this.currentImage !== null;
27521
+ }
27522
+ /**
27523
+ * Update the pane from current editor state.
27524
+ */
27525
+ update() {
27526
+ this.updateFromSelection();
27527
+ }
27528
+ }
27529
+
27530
+ /**
27531
+ * TablePane - Edit table properties.
27532
+ *
27533
+ * Shows:
27534
+ * - Table structure (row/column count)
27535
+ * - Row/column insertion/removal
27536
+ * - Header rows/columns
27537
+ * - Default cell padding and border color
27538
+ * - Cell-specific formatting (background, borders)
27539
+ *
27540
+ * Uses the PCEditor public API:
27541
+ * - editor.getFocusedTable()
27542
+ * - editor.tableInsertRow()
27543
+ * - editor.tableRemoveRow()
27544
+ * - editor.tableInsertColumn()
27545
+ * - editor.tableRemoveColumn()
27546
+ *
27547
+ * And TableObject methods:
27548
+ * - table.setHeaderRowCount()
27549
+ * - table.setHeaderColumnCount()
27550
+ * - table.getCell()
27551
+ * - table.getCellsInRange()
27552
+ */
27553
+ class TablePane extends BasePane {
27554
+ constructor(id = 'table', options = {}) {
27555
+ super(id, { className: 'pc-pane-table', ...options });
27556
+ // Structure info
27557
+ this.rowCountDisplay = null;
27558
+ this.colCountDisplay = null;
27559
+ this.cellSelectionDisplay = null;
27560
+ // Header controls
27561
+ this.headerRowInput = null;
27562
+ this.headerColInput = null;
27563
+ // Default controls
27564
+ this.defaultPaddingInput = null;
27565
+ this.defaultBorderColorInput = null;
27566
+ // Cell formatting controls
27567
+ this.cellBgColorInput = null;
27568
+ this.borderTopCheck = null;
27569
+ this.borderRightCheck = null;
27570
+ this.borderBottomCheck = null;
27571
+ this.borderLeftCheck = null;
27572
+ this.borderWidthInput = null;
27573
+ this.borderColorInput = null;
27574
+ this.borderStyleSelect = null;
27575
+ this.currentTable = null;
27576
+ this.onApplyCallback = options.onApply;
27577
+ }
27578
+ attach(options) {
27579
+ super.attach(options);
27580
+ if (this.editor) {
27581
+ // Listen for selection/focus changes
27582
+ const updateHandler = () => this.updateFromFocusedTable();
27583
+ this.editor.on('selection-change', updateHandler);
27584
+ this.editor.on('table-cell-focus', updateHandler);
27585
+ this.editor.on('table-cell-selection', updateHandler);
27586
+ this.eventCleanup.push(() => {
27587
+ this.editor?.off('selection-change', updateHandler);
27588
+ this.editor?.off('table-cell-focus', updateHandler);
27589
+ this.editor?.off('table-cell-selection', updateHandler);
27590
+ });
27591
+ // Initial update
27592
+ this.updateFromFocusedTable();
27593
+ }
27594
+ }
27595
+ createContent() {
27596
+ const container = document.createElement('div');
27597
+ // Structure section
27598
+ const structureSection = this.createSection('Structure');
27599
+ const structureInfo = document.createElement('div');
27600
+ structureInfo.className = 'pc-pane-info-list';
27601
+ this.rowCountDisplay = document.createElement('span');
27602
+ this.colCountDisplay = document.createElement('span');
27603
+ const rowInfo = document.createElement('div');
27604
+ rowInfo.className = 'pc-pane-info';
27605
+ rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27606
+ rowInfo.appendChild(this.rowCountDisplay);
27607
+ const colInfo = document.createElement('div');
27608
+ colInfo.className = 'pc-pane-info';
27609
+ colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27610
+ colInfo.appendChild(this.colCountDisplay);
27611
+ structureInfo.appendChild(rowInfo);
27612
+ structureInfo.appendChild(colInfo);
27613
+ structureSection.appendChild(structureInfo);
27614
+ // Row/column buttons
27615
+ const structureBtns = this.createButtonGroup();
27616
+ const addRowBtn = this.createButton('+ Row');
27617
+ this.addButtonListener(addRowBtn, () => this.insertRow());
27618
+ const removeRowBtn = this.createButton('- Row');
27619
+ this.addButtonListener(removeRowBtn, () => this.removeRow());
27620
+ const addColBtn = this.createButton('+ Column');
27621
+ this.addButtonListener(addColBtn, () => this.insertColumn());
27622
+ const removeColBtn = this.createButton('- Column');
27623
+ this.addButtonListener(removeColBtn, () => this.removeColumn());
27624
+ structureBtns.appendChild(addRowBtn);
27625
+ structureBtns.appendChild(removeRowBtn);
27626
+ structureBtns.appendChild(addColBtn);
27627
+ structureBtns.appendChild(removeColBtn);
27628
+ structureSection.appendChild(structureBtns);
27629
+ container.appendChild(structureSection);
27630
+ // Headers section
27631
+ const headersSection = this.createSection('Headers');
27632
+ const headerRow = this.createRow();
27633
+ this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27634
+ this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27635
+ headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27636
+ headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27637
+ headersSection.appendChild(headerRow);
27638
+ const applyHeadersBtn = this.createButton('Apply Headers');
27639
+ this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27640
+ headersSection.appendChild(applyHeadersBtn);
27641
+ container.appendChild(headersSection);
27642
+ // Defaults section
27643
+ const defaultsSection = this.createSection('Defaults');
27644
+ const defaultsRow = this.createRow();
27645
+ this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
27646
+ this.defaultBorderColorInput = this.createColorInput('#cccccc');
27647
+ defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27648
+ defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27649
+ defaultsSection.appendChild(defaultsRow);
27650
+ const applyDefaultsBtn = this.createButton('Apply Defaults');
27651
+ this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27652
+ defaultsSection.appendChild(applyDefaultsBtn);
27653
+ container.appendChild(defaultsSection);
27654
+ // Cell formatting section
27655
+ const cellSection = this.createSection('Cell Formatting');
27656
+ this.cellSelectionDisplay = this.createHint('No cell selected');
27657
+ cellSection.appendChild(this.cellSelectionDisplay);
27658
+ // Background
27659
+ this.cellBgColorInput = this.createColorInput('#ffffff');
27660
+ cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
27661
+ // Border checkboxes
27662
+ const borderChecks = document.createElement('div');
27663
+ borderChecks.className = 'pc-pane-row';
27664
+ borderChecks.style.flexWrap = 'wrap';
27665
+ borderChecks.style.gap = '4px';
27666
+ this.borderTopCheck = document.createElement('input');
27667
+ this.borderTopCheck.type = 'checkbox';
27668
+ this.borderTopCheck.checked = true;
27669
+ this.borderRightCheck = document.createElement('input');
27670
+ this.borderRightCheck.type = 'checkbox';
27671
+ this.borderRightCheck.checked = true;
27672
+ this.borderBottomCheck = document.createElement('input');
27673
+ this.borderBottomCheck.type = 'checkbox';
27674
+ this.borderBottomCheck.checked = true;
27675
+ this.borderLeftCheck = document.createElement('input');
27676
+ this.borderLeftCheck.type = 'checkbox';
27677
+ this.borderLeftCheck.checked = true;
27678
+ borderChecks.appendChild(this.createCheckbox('Top', true));
27679
+ borderChecks.appendChild(this.createCheckbox('Right', true));
27680
+ borderChecks.appendChild(this.createCheckbox('Bottom', true));
27681
+ borderChecks.appendChild(this.createCheckbox('Left', true));
27682
+ // Replace created checkboxes with our tracked ones
27683
+ const checkLabels = borderChecks.querySelectorAll('label');
27684
+ if (checkLabels[0])
27685
+ checkLabels[0].replaceChild(this.borderTopCheck, checkLabels[0].querySelector('input'));
27686
+ if (checkLabels[1])
27687
+ checkLabels[1].replaceChild(this.borderRightCheck, checkLabels[1].querySelector('input'));
27688
+ if (checkLabels[2])
27689
+ checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
27690
+ if (checkLabels[3])
27691
+ checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
27692
+ cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
27693
+ // Border properties
27694
+ const borderPropsRow = this.createRow();
27695
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
27696
+ this.borderColorInput = this.createColorInput('#cccccc');
27697
+ borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27698
+ borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27699
+ cellSection.appendChild(borderPropsRow);
27700
+ this.borderStyleSelect = this.createSelect([
27701
+ { value: 'solid', label: 'Solid' },
27702
+ { value: 'dashed', label: 'Dashed' },
27703
+ { value: 'dotted', label: 'Dotted' },
27704
+ { value: 'none', label: 'None' }
27705
+ ], 'solid');
27706
+ cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27707
+ const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
27708
+ this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
27709
+ cellSection.appendChild(applyCellBtn);
27710
+ container.appendChild(cellSection);
27711
+ return container;
27712
+ }
27713
+ updateFromFocusedTable() {
27714
+ if (!this.editor)
27715
+ return;
27716
+ const table = this.editor.getFocusedTable();
27717
+ if (table) {
27718
+ this.showTable(table);
27719
+ }
27720
+ else {
27721
+ this.hideTable();
27722
+ }
27723
+ }
27724
+ /**
27725
+ * Show the pane with the given table.
27726
+ */
27727
+ showTable(table) {
27728
+ this.currentTable = table;
27729
+ // Update structure info
27730
+ if (this.rowCountDisplay) {
27731
+ this.rowCountDisplay.textContent = String(table.rowCount);
27732
+ this.rowCountDisplay.className = 'pc-pane-info-value';
27733
+ }
27734
+ if (this.colCountDisplay) {
27735
+ this.colCountDisplay.textContent = String(table.columnCount);
27736
+ this.colCountDisplay.className = 'pc-pane-info-value';
27737
+ }
27738
+ // Update header counts
27739
+ if (this.headerRowInput) {
27740
+ this.headerRowInput.value = String(table.headerRowCount);
27741
+ }
27742
+ if (this.headerColInput) {
27743
+ this.headerColInput.value = String(table.headerColumnCount);
27744
+ }
27745
+ // Update defaults
27746
+ if (this.defaultPaddingInput) {
27747
+ this.defaultPaddingInput.value = String(table.defaultCellPadding);
27748
+ }
27749
+ if (this.defaultBorderColorInput) {
27750
+ this.defaultBorderColorInput.value = table.defaultBorderColor;
27751
+ }
27752
+ // Update cell selection info
27753
+ this.updateCellSelectionInfo(table);
27754
+ this.show();
27755
+ }
27756
+ /**
27757
+ * Hide the pane and clear current table.
27758
+ */
27759
+ hideTable() {
27760
+ this.currentTable = null;
27761
+ this.hide();
27762
+ }
27763
+ updateCellSelectionInfo(table) {
27764
+ if (!this.cellSelectionDisplay)
27765
+ return;
27766
+ const focusedCell = table.focusedCell;
27767
+ const selectedRange = table.selectedRange;
27768
+ if (selectedRange) {
27769
+ const count = (selectedRange.end.row - selectedRange.start.row + 1) *
27770
+ (selectedRange.end.col - selectedRange.start.col + 1);
27771
+ this.cellSelectionDisplay.textContent = `${count} cells selected`;
27772
+ }
27773
+ else if (focusedCell) {
27774
+ this.cellSelectionDisplay.textContent = `Cell [${focusedCell.row}, ${focusedCell.col}]`;
27775
+ // Update cell formatting controls from focused cell
27776
+ const cell = table.getCell(focusedCell.row, focusedCell.col);
27777
+ if (cell) {
27778
+ if (this.cellBgColorInput) {
27779
+ this.cellBgColorInput.value = cell.backgroundColor || '#ffffff';
27780
+ }
27781
+ // Update border controls
27782
+ const border = cell.border;
27783
+ if (this.borderTopCheck)
27784
+ this.borderTopCheck.checked = border.top.style !== 'none';
27785
+ if (this.borderRightCheck)
27786
+ this.borderRightCheck.checked = border.right.style !== 'none';
27787
+ if (this.borderBottomCheck)
27788
+ this.borderBottomCheck.checked = border.bottom.style !== 'none';
27789
+ if (this.borderLeftCheck)
27790
+ this.borderLeftCheck.checked = border.left.style !== 'none';
27791
+ // Use first active border for properties
27792
+ const activeBorder = border.top.style !== 'none' ? border.top :
27793
+ border.right.style !== 'none' ? border.right :
27794
+ border.bottom.style !== 'none' ? border.bottom :
27795
+ border.left.style !== 'none' ? border.left : border.top;
27796
+ if (this.borderWidthInput)
27797
+ this.borderWidthInput.value = String(activeBorder.width);
27798
+ if (this.borderColorInput)
27799
+ this.borderColorInput.value = activeBorder.color;
27800
+ if (this.borderStyleSelect)
27801
+ this.borderStyleSelect.value = activeBorder.style;
27802
+ }
27803
+ }
27804
+ else {
27805
+ this.cellSelectionDisplay.textContent = 'No cell selected';
27806
+ }
27807
+ }
27808
+ insertRow() {
27809
+ if (!this.editor || !this.currentTable)
27810
+ return;
27811
+ const focusedCell = this.currentTable.focusedCell;
27812
+ const rowIndex = focusedCell ? focusedCell.row + 1 : this.currentTable.rowCount;
27813
+ this.editor.tableInsertRow(this.currentTable, rowIndex);
27814
+ this.updateFromFocusedTable();
27815
+ }
27816
+ removeRow() {
27817
+ if (!this.editor || !this.currentTable)
27818
+ return;
27819
+ const focusedCell = this.currentTable.focusedCell;
27820
+ if (focusedCell && this.currentTable.rowCount > 1) {
27821
+ this.editor.tableRemoveRow(this.currentTable, focusedCell.row);
27822
+ this.updateFromFocusedTable();
27823
+ }
27824
+ }
27825
+ insertColumn() {
27826
+ if (!this.editor || !this.currentTable)
27827
+ return;
27828
+ const focusedCell = this.currentTable.focusedCell;
27829
+ const colIndex = focusedCell ? focusedCell.col + 1 : this.currentTable.columnCount;
27830
+ this.editor.tableInsertColumn(this.currentTable, colIndex);
27831
+ this.updateFromFocusedTable();
27832
+ }
27833
+ removeColumn() {
27834
+ if (!this.editor || !this.currentTable)
27835
+ return;
27836
+ const focusedCell = this.currentTable.focusedCell;
27837
+ if (focusedCell && this.currentTable.columnCount > 1) {
27838
+ this.editor.tableRemoveColumn(this.currentTable, focusedCell.col);
27839
+ this.updateFromFocusedTable();
27840
+ }
27841
+ }
27842
+ applyHeaders() {
27843
+ if (!this.currentTable)
27844
+ return;
27845
+ if (this.headerRowInput) {
27846
+ const count = parseInt(this.headerRowInput.value, 10);
27847
+ this.currentTable.setHeaderRowCount(count);
27848
+ }
27849
+ if (this.headerColInput) {
27850
+ const count = parseInt(this.headerColInput.value, 10);
27851
+ this.currentTable.setHeaderColumnCount(count);
27852
+ }
27853
+ this.editor?.render();
27854
+ this.onApplyCallback?.(true);
27855
+ }
27856
+ applyDefaults() {
27857
+ if (!this.currentTable)
27858
+ return;
27859
+ if (this.defaultPaddingInput) {
27860
+ this.currentTable.defaultCellPadding = parseInt(this.defaultPaddingInput.value, 10);
27861
+ }
27862
+ if (this.defaultBorderColorInput) {
27863
+ this.currentTable.defaultBorderColor = this.defaultBorderColorInput.value;
27864
+ }
27865
+ this.editor?.render();
27866
+ this.onApplyCallback?.(true);
27867
+ }
27868
+ applyCellFormatting() {
27869
+ if (!this.currentTable)
27870
+ return;
27871
+ const focusedCell = this.currentTable.focusedCell;
27872
+ const selectedRange = this.currentTable.selectedRange;
27873
+ // Determine cells to update
27874
+ const cells = [];
27875
+ if (selectedRange) {
27876
+ for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
27877
+ for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
27878
+ cells.push({ row, col });
27879
+ }
27880
+ }
27881
+ }
27882
+ else if (focusedCell) {
27883
+ cells.push(focusedCell);
27884
+ }
27885
+ if (cells.length === 0)
27886
+ return;
27887
+ // Build border config
27888
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27889
+ const color = this.borderColorInput?.value || '#cccccc';
27890
+ const style = (this.borderStyleSelect?.value || 'solid');
27891
+ const borderSide = { width, color, style };
27892
+ const noneBorder = { width: 0, color: '#000000', style: 'none' };
27893
+ const border = {
27894
+ top: this.borderTopCheck?.checked ? { ...borderSide } : { ...noneBorder },
27895
+ right: this.borderRightCheck?.checked ? { ...borderSide } : { ...noneBorder },
27896
+ bottom: this.borderBottomCheck?.checked ? { ...borderSide } : { ...noneBorder },
27897
+ left: this.borderLeftCheck?.checked ? { ...borderSide } : { ...noneBorder }
27898
+ };
27899
+ const bgColor = this.cellBgColorInput?.value;
27900
+ // Apply to each cell
27901
+ for (const { row, col } of cells) {
27902
+ const cell = this.currentTable.getCell(row, col);
27903
+ if (cell) {
27904
+ if (bgColor) {
27905
+ cell.backgroundColor = bgColor;
27906
+ }
27907
+ cell.border = border;
27908
+ }
27909
+ }
27910
+ this.editor?.render();
27911
+ this.onApplyCallback?.(true);
27912
+ }
27913
+ /**
27914
+ * Get the currently focused table.
27915
+ */
27916
+ getCurrentTable() {
27917
+ return this.currentTable;
27918
+ }
27919
+ /**
27920
+ * Check if a table is currently focused.
27921
+ */
27922
+ hasTable() {
27923
+ return this.currentTable !== null;
27924
+ }
27925
+ /**
27926
+ * Update the pane from current editor state.
27927
+ */
27928
+ update() {
27929
+ this.updateFromFocusedTable();
27930
+ }
27931
+ }
27932
+
27933
+ export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
25162
27934
  //# sourceMappingURL=pc-editor.esm.js.map