@productcloudos/editor 1.0.1 → 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 (57) hide show
  1. package/README.md +1 -0
  2. package/dist/pc-editor.esm.js +3157 -43
  3. package/dist/pc-editor.esm.js.map +1 -1
  4. package/dist/pc-editor.js +3169 -42
  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/objects/ImageObject.d.ts.map +1 -1
  13. package/dist/types/lib/objects/table/TableObject.d.ts +5 -0
  14. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  15. package/dist/types/lib/panes/BasePane.d.ts +117 -0
  16. package/dist/types/lib/panes/BasePane.d.ts.map +1 -0
  17. package/dist/types/lib/panes/DocumentInfoPane.d.ts +24 -0
  18. package/dist/types/lib/panes/DocumentInfoPane.d.ts.map +1 -0
  19. package/dist/types/lib/panes/DocumentSettingsPane.d.ts +28 -0
  20. package/dist/types/lib/panes/DocumentSettingsPane.d.ts.map +1 -0
  21. package/dist/types/lib/panes/FormattingPane.d.ts +82 -0
  22. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -0
  23. package/dist/types/lib/panes/HyperlinkPane.d.ts +66 -0
  24. package/dist/types/lib/panes/HyperlinkPane.d.ts.map +1 -0
  25. package/dist/types/lib/panes/ImagePane.d.ts +79 -0
  26. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -0
  27. package/dist/types/lib/panes/MergeDataPane.d.ts +55 -0
  28. package/dist/types/lib/panes/MergeDataPane.d.ts.map +1 -0
  29. package/dist/types/lib/panes/RepeatingSectionPane.d.ts +62 -0
  30. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -0
  31. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts +65 -0
  32. package/dist/types/lib/panes/SubstitutionFieldPane.d.ts.map +1 -0
  33. package/dist/types/lib/panes/TablePane.d.ts +88 -0
  34. package/dist/types/lib/panes/TablePane.d.ts.map +1 -0
  35. package/dist/types/lib/panes/TableRowLoopPane.d.ts +68 -0
  36. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -0
  37. package/dist/types/lib/panes/TextBoxPane.d.ts +67 -0
  38. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -0
  39. package/dist/types/lib/panes/ViewSettingsPane.d.ts +52 -0
  40. package/dist/types/lib/panes/ViewSettingsPane.d.ts.map +1 -0
  41. package/dist/types/lib/panes/index.d.ts +34 -0
  42. package/dist/types/lib/panes/index.d.ts.map +1 -0
  43. package/dist/types/lib/panes/types.d.ts +111 -0
  44. package/dist/types/lib/panes/types.d.ts.map +1 -0
  45. package/dist/types/lib/rendering/CanvasManager.d.ts +9 -0
  46. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  47. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +16 -1
  48. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  49. package/dist/types/lib/rendering/PDFGenerator.d.ts +16 -0
  50. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  51. package/dist/types/lib/text/TextFormatting.d.ts +9 -0
  52. package/dist/types/lib/text/TextFormatting.d.ts.map +1 -1
  53. package/dist/types/lib/text/TextLayout.d.ts.map +1 -1
  54. package/dist/types/lib/text/types.d.ts +1 -0
  55. package/dist/types/lib/text/types.d.ts.map +1 -1
  56. package/dist/types/lib/undo/transaction/MutationUndo.d.ts.map +1 -1
  57. package/package.json +1 -1
package/dist/pc-editor.js CHANGED
@@ -864,6 +864,20 @@ class TextFormattingManager extends EventEmitter {
864
864
  this.emit('formatting-changed', { start, end, formatting });
865
865
  }
866
866
  }
867
+ /**
868
+ * Set formatting at a specific position, replacing all existing formatting.
869
+ * Unlike applyFormatting which merges, this replaces completely.
870
+ * Used by undo operations to restore exact previous state.
871
+ * @param position Character position
872
+ * @param formatting Complete formatting to set
873
+ * @param silent If true, don't emit the formatting-changed event
874
+ */
875
+ setFormattingAt(position, formatting, silent = false) {
876
+ this.formatting.set(position, { ...formatting });
877
+ if (!silent) {
878
+ this.emit('formatting-changed', { start: position, end: position + 1, formatting });
879
+ }
880
+ }
867
881
  /**
868
882
  * Remove formatting from a range, reverting to default.
869
883
  */
@@ -2564,11 +2578,14 @@ class TextLayout {
2564
2578
  const emptyLine = this.createEmptyLine(startIndex, formatting, alignment);
2565
2579
  // Add list marker to empty line if it's a list item
2566
2580
  if (listFormatting) {
2581
+ // Get formatting for the empty line (inherited from previous position)
2582
+ const markerFormatting = formatting.getFormattingAt(startIndex > 0 ? startIndex - 1 : startIndex);
2567
2583
  emptyLine.listMarker = {
2568
2584
  text: markerText,
2569
2585
  width: markerWidth,
2570
2586
  indent: listIndent,
2571
- isFirstLineOfListItem: true
2587
+ isFirstLineOfListItem: true,
2588
+ formatting: markerFormatting
2572
2589
  };
2573
2590
  }
2574
2591
  lines.push(emptyLine);
@@ -3857,6 +3874,7 @@ class ImageObject extends BaseEmbeddedObject {
3857
3874
  const imgHeight = this._image.naturalHeight;
3858
3875
  let sx = 0, sy = 0, sw = imgWidth, sh = imgHeight;
3859
3876
  let dx = 0, dy = 0, dw = width, dh = height;
3877
+ let needsClipping = false;
3860
3878
  switch (this._fit) {
3861
3879
  case 'fill':
3862
3880
  // Stretch to fill
@@ -3880,18 +3898,31 @@ class ImageObject extends BaseEmbeddedObject {
3880
3898
  break;
3881
3899
  }
3882
3900
  case 'none':
3883
- // Original size, centered
3901
+ // Original size, centered - clip to box bounds if image is larger
3884
3902
  dw = imgWidth;
3885
3903
  dh = imgHeight;
3886
3904
  dx = (width - imgWidth) / 2;
3887
3905
  dy = (height - imgHeight) / 2;
3906
+ // Need clipping if image extends beyond bounds
3907
+ if (imgWidth > width || imgHeight > height) {
3908
+ needsClipping = true;
3909
+ }
3888
3910
  break;
3889
3911
  case 'tile':
3890
3912
  // Tile the image to fill bounds
3891
3913
  this.drawTiledImage(ctx, width, height);
3892
3914
  return; // Early return, tiling handles its own drawing
3893
3915
  }
3916
+ if (needsClipping) {
3917
+ ctx.save();
3918
+ ctx.beginPath();
3919
+ ctx.rect(0, 0, width, height);
3920
+ ctx.clip();
3921
+ }
3894
3922
  ctx.drawImage(this._image, sx, sy, sw, sh, dx, dy, dw, dh);
3923
+ if (needsClipping) {
3924
+ ctx.restore();
3925
+ }
3895
3926
  }
3896
3927
  drawTiledImage(ctx, width, height) {
3897
3928
  if (!this._image)
@@ -6407,8 +6438,9 @@ class TableObject extends BaseEmbeddedObject {
6407
6438
  }
6408
6439
  x += width;
6409
6440
  }
6410
- if (targetCol === -1)
6441
+ if (targetCol === -1) {
6411
6442
  return null;
6443
+ }
6412
6444
  // Calculate row positions
6413
6445
  let y = 0;
6414
6446
  let targetRow = -1;
@@ -6546,6 +6578,18 @@ class TableObject extends BaseEmbeddedObject {
6546
6578
  for (const row of this._rows) {
6547
6579
  row.removeCell(colIndex);
6548
6580
  }
6581
+ // Clear selection if it references the deleted column or is now out of bounds
6582
+ if (this._selectedRange) {
6583
+ const { start, end } = this._selectedRange;
6584
+ if (start.col >= this._columns.length || end.col >= this._columns.length ||
6585
+ (colIndex >= start.col && colIndex <= end.col)) {
6586
+ this._selectedRange = null;
6587
+ }
6588
+ }
6589
+ // Clear focused cell if it references the deleted column or is now out of bounds
6590
+ if (this._focusedCell && this._focusedCell.col >= this._columns.length) {
6591
+ this._focusedCell = null;
6592
+ }
6549
6593
  this._layoutDirty = true;
6550
6594
  this.updateCoveredCells();
6551
6595
  this.updateSizeFromLayout();
@@ -6604,6 +6648,18 @@ class TableObject extends BaseEmbeddedObject {
6604
6648
  const [removed] = this._rows.splice(rowIndex, 1);
6605
6649
  // Adjust row loop indices
6606
6650
  this.shiftRowLoopIndices(rowIndex, -1);
6651
+ // Clear selection if it references the deleted row or is now out of bounds
6652
+ if (this._selectedRange) {
6653
+ const { start, end } = this._selectedRange;
6654
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6655
+ (rowIndex >= start.row && rowIndex <= end.row)) {
6656
+ this._selectedRange = null;
6657
+ }
6658
+ }
6659
+ // Clear focused cell if it references the deleted row or is now out of bounds
6660
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6661
+ this._focusedCell = null;
6662
+ }
6607
6663
  this._layoutDirty = true;
6608
6664
  this.updateCoveredCells();
6609
6665
  this.updateSizeFromLayout();
@@ -6775,6 +6831,18 @@ class TableObject extends BaseEmbeddedObject {
6775
6831
  removeRowsInRange(startIndex, endIndex) {
6776
6832
  const count = endIndex - startIndex + 1;
6777
6833
  const removed = this._rows.splice(startIndex, count);
6834
+ // Clear selection if it overlaps with removed rows or is now out of bounds
6835
+ if (this._selectedRange) {
6836
+ const { start, end } = this._selectedRange;
6837
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6838
+ (start.row <= endIndex && end.row >= startIndex)) {
6839
+ this._selectedRange = null;
6840
+ }
6841
+ }
6842
+ // Clear focused cell if it's now out of bounds
6843
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6844
+ this._focusedCell = null;
6845
+ }
6778
6846
  this._layoutDirty = true;
6779
6847
  this.updateCoveredCells();
6780
6848
  return removed;
@@ -7198,8 +7266,74 @@ class TableObject extends BaseEmbeddedObject {
7198
7266
  }
7199
7267
  y += row.calculatedHeight;
7200
7268
  }
7269
+ // Render cell range selection highlight for this slice
7270
+ if (this._selectedRange) {
7271
+ this.renderRangeSelectionForSlice(ctx, slice, pageLayout);
7272
+ }
7201
7273
  // Note: Selection border is drawn by FlowingTextRenderer with correct slice height
7202
7274
  }
7275
+ /**
7276
+ * Render range selection highlight for a specific slice.
7277
+ * Only renders the portion of the selection that's visible in this slice.
7278
+ */
7279
+ renderRangeSelectionForSlice(ctx, slice, pageLayout) {
7280
+ if (!this._selectedRange)
7281
+ return;
7282
+ const { start, end } = this._selectedRange;
7283
+ const columnPositions = this.getColumnPositions();
7284
+ const columnWidths = this.getColumnWidths();
7285
+ // Calculate X bounds (same for all slices)
7286
+ const x1 = columnPositions[start.col];
7287
+ const x2 = columnPositions[end.col] + columnWidths[end.col];
7288
+ // Build a map of row index -> Y position in this slice's coordinate system
7289
+ const rowYInSlice = new Map();
7290
+ let y = 0;
7291
+ // On continuation pages, header rows are at the top
7292
+ if (slice.isContinuation && pageLayout.headerRowIndices.length > 0) {
7293
+ for (const headerRowIdx of pageLayout.headerRowIndices) {
7294
+ const row = this._rows[headerRowIdx];
7295
+ if (row) {
7296
+ rowYInSlice.set(headerRowIdx, y);
7297
+ y += row.calculatedHeight;
7298
+ }
7299
+ }
7300
+ }
7301
+ // Data rows for this slice
7302
+ for (let rowIdx = slice.startRow; rowIdx < slice.endRow; rowIdx++) {
7303
+ const row = this._rows[rowIdx];
7304
+ if (!row)
7305
+ continue;
7306
+ if (slice.isContinuation && row.isHeader)
7307
+ continue; // Skip headers, already added
7308
+ rowYInSlice.set(rowIdx, y);
7309
+ y += row.calculatedHeight;
7310
+ }
7311
+ // Check if any selected rows are visible in this slice
7312
+ let y1 = null;
7313
+ let y2 = null;
7314
+ for (let rowIdx = start.row; rowIdx <= end.row; rowIdx++) {
7315
+ const rowY = rowYInSlice.get(rowIdx);
7316
+ if (rowY !== undefined) {
7317
+ const row = this._rows[rowIdx];
7318
+ if (row) {
7319
+ if (y1 === null)
7320
+ y1 = rowY;
7321
+ y2 = rowY + row.calculatedHeight;
7322
+ }
7323
+ }
7324
+ }
7325
+ // If no selected rows are visible in this slice, don't render
7326
+ if (y1 === null || y2 === null)
7327
+ return;
7328
+ // Draw selection highlight
7329
+ ctx.fillStyle = 'rgba(0, 120, 215, 0.2)';
7330
+ ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
7331
+ // Draw selection border
7332
+ ctx.strokeStyle = 'rgba(0, 120, 215, 0.8)';
7333
+ ctx.lineWidth = 2;
7334
+ ctx.setLineDash([]);
7335
+ ctx.strokeRect(x1 + 1, y1 + 1, x2 - x1 - 2, y2 - y1 - 2);
7336
+ }
7203
7337
  /**
7204
7338
  * Get rows that should be rendered for a specific page slice.
7205
7339
  * Includes header rows on continuation pages.
@@ -7366,6 +7500,8 @@ class TableObject extends BaseEmbeddedObject {
7366
7500
  }
7367
7501
  blur() {
7368
7502
  this.editing = false;
7503
+ // Clear cell range selection when table loses focus
7504
+ this.clearSelection();
7369
7505
  this.emit('blur', {});
7370
7506
  }
7371
7507
  hasFocus() {
@@ -11432,7 +11568,9 @@ class FlowingTextRenderer extends EventEmitter {
11432
11568
  const maxWidth = this.getAvailableWidthForRegion(region, pageIndex);
11433
11569
  const flowingContent = region.flowingContent;
11434
11570
  // Get cursor position for field selection highlighting
11435
- const cursorTextIndex = flowingContent.getCursorPosition();
11571
+ // Only use cursor position if the content has focus (otherwise fields stay "selected")
11572
+ const hasFocus = flowingContent.hasFocus();
11573
+ const cursorTextIndex = hasFocus ? flowingContent.getCursorPosition() : undefined;
11436
11574
  // Setup clipping if requested (useful for text boxes)
11437
11575
  if (clipToBounds) {
11438
11576
  ctx.save();
@@ -11469,7 +11607,7 @@ class FlowingTextRenderer extends EventEmitter {
11469
11607
  }
11470
11608
  // Render cursor if this region is active and cursor should be shown
11471
11609
  if (renderCursor && flowingContent.hasFocus() && flowingContent.isCursorVisible()) {
11472
- this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, cursorTextIndex);
11610
+ this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, flowingContent.getCursorPosition());
11473
11611
  }
11474
11612
  if (clipToBounds) {
11475
11613
  ctx.restore();
@@ -11577,8 +11715,10 @@ class FlowingTextRenderer extends EventEmitter {
11577
11715
  this.pageTextOffsets.set(pageIndex, 0);
11578
11716
  }
11579
11717
  // Get cursor position from the specified FlowingTextContent, or fall back to body
11718
+ // Only use cursor position for field selection if the content has focus
11580
11719
  const contentForCursor = flowingContent || this.document.bodyFlowingContent;
11581
- const cursorTextIndex = contentForCursor ? contentForCursor.getCursorPosition() : 0;
11720
+ const hasFocus = contentForCursor?.hasFocus() ?? false;
11721
+ const cursorTextIndex = hasFocus && contentForCursor ? contentForCursor.getCursorPosition() : undefined;
11582
11722
  // Get total page count for page count fields
11583
11723
  const firstPage = this.document.pages[0];
11584
11724
  const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
@@ -11660,7 +11800,7 @@ class FlowingTextRenderer extends EventEmitter {
11660
11800
  ctx.textBaseline = 'alphabetic';
11661
11801
  // Render list marker if this is the first line of a list item
11662
11802
  if (line.listMarker?.isFirstLineOfListItem && line.listMarker.text) {
11663
- this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting);
11803
+ this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting || line.listMarker.formatting);
11664
11804
  }
11665
11805
  // Create maps for quick lookup by text index
11666
11806
  const substitutionFieldMap = new Map();
@@ -12894,11 +13034,14 @@ class FlowingTextRenderer extends EventEmitter {
12894
13034
  for (const line of flowedPage.lines) {
12895
13035
  if (this.lineContainsSelection(line, this.selectedText)) {
12896
13036
  const selectionBounds = this.getSelectionBoundsInLine(line, this.selectedText);
12897
- // Add alignment offset to position the selection correctly
12898
- const alignmentOffset = this.getAlignmentOffset(line, bounds.width);
12899
- // Account for list indentation
13037
+ // Account for list indentation - must match renderFlowedLine calculation
12900
13038
  const listIndent = line.listMarker?.indent ?? 0;
12901
- ctx.fillRect(bounds.x + alignmentOffset + listIndent + selectionBounds.x, y, selectionBounds.width, line.height);
13039
+ // Calculate alignment offset using effective width (excluding list indent)
13040
+ // This matches how renderFlowedLine calculates alignment
13041
+ const effectiveMaxWidth = bounds.width - listIndent;
13042
+ const alignmentOffset = this.getAlignmentOffset(line, effectiveMaxWidth);
13043
+ const baseX = bounds.x + listIndent;
13044
+ ctx.fillRect(baseX + alignmentOffset + selectionBounds.x, y, selectionBounds.width, line.height);
12902
13045
  }
12903
13046
  y += line.height;
12904
13047
  }
@@ -13371,15 +13514,34 @@ class FlowingTextRenderer extends EventEmitter {
13371
13514
  }
13372
13515
  /**
13373
13516
  * Get a complete snapshot of all flowed content for PDF export.
13374
- * Returns body pages, header, and footer content.
13517
+ * Returns body pages, header, footer content, and hyperlinks.
13375
13518
  */
13376
13519
  getFlowedPagesSnapshot() {
13377
13520
  const firstPage = this.document.pages[0];
13378
13521
  const bodyPages = firstPage ? this.flowedPages.get(firstPage.id) || [] : [];
13522
+ // Extract hyperlinks from each content area
13523
+ const bodyHyperlinks = this.document.bodyFlowingContent?.getAllHyperlinks().map(h => ({
13524
+ url: h.url,
13525
+ startIndex: h.startIndex,
13526
+ endIndex: h.endIndex
13527
+ }));
13528
+ const headerHyperlinks = this.document.headerFlowingContent?.getAllHyperlinks().map(h => ({
13529
+ url: h.url,
13530
+ startIndex: h.startIndex,
13531
+ endIndex: h.endIndex
13532
+ }));
13533
+ const footerHyperlinks = this.document.footerFlowingContent?.getAllHyperlinks().map(h => ({
13534
+ url: h.url,
13535
+ startIndex: h.startIndex,
13536
+ endIndex: h.endIndex
13537
+ }));
13379
13538
  return {
13380
13539
  body: bodyPages,
13381
13540
  header: this.headerFlowedPage,
13382
- footer: this.footerFlowedPage
13541
+ footer: this.footerFlowedPage,
13542
+ bodyHyperlinks: bodyHyperlinks?.length ? bodyHyperlinks : undefined,
13543
+ headerHyperlinks: headerHyperlinks?.length ? headerHyperlinks : undefined,
13544
+ footerHyperlinks: footerHyperlinks?.length ? footerHyperlinks : undefined
13383
13545
  };
13384
13546
  }
13385
13547
  // ============================================
@@ -13809,6 +13971,25 @@ class CanvasManager extends EventEmitter {
13809
13971
  this.canvases.clear();
13810
13972
  this.contexts.clear();
13811
13973
  }
13974
+ /**
13975
+ * Update canvas sizes to match current page dimensions.
13976
+ * Call this when page size or orientation changes.
13977
+ */
13978
+ updateCanvasSizes() {
13979
+ this.document.pages.forEach(page => {
13980
+ const canvas = this.canvases.get(page.id);
13981
+ if (!canvas)
13982
+ return;
13983
+ const dimensions = page.getPageDimensions();
13984
+ // Only update if dimensions have changed
13985
+ if (canvas.width !== dimensions.width || canvas.height !== dimensions.height) {
13986
+ canvas.width = dimensions.width;
13987
+ canvas.height = dimensions.height;
13988
+ }
13989
+ });
13990
+ // Update zoom scale to account for new dimensions
13991
+ this.updateCanvasScale();
13992
+ }
13812
13993
  render() {
13813
13994
  this.document.pages.forEach(page => {
13814
13995
  const ctx = this.contexts.get(page.id);
@@ -14148,6 +14329,12 @@ class CanvasManager extends EventEmitter {
14148
14329
  const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
14149
14330
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14150
14331
  const object = embeddedObjectHit.data.object;
14332
+ // Check which section the object belongs to - only interact if in active section
14333
+ const objectSection = this.getSectionForEmbeddedObject(object);
14334
+ if (objectSection && objectSection !== this._activeSection) {
14335
+ // Object is in a different section - ignore the interaction
14336
+ return;
14337
+ }
14151
14338
  // For relative-positioned objects, prepare for potential drag
14152
14339
  // Don't start drag immediately - wait for threshold to allow double-click
14153
14340
  if (object.position === 'relative') {
@@ -14316,19 +14503,40 @@ class CanvasManager extends EventEmitter {
14316
14503
  // Handle table cell selection drag
14317
14504
  if (this.isSelectingTableCells && this.tableCellSelectionStart && this.tableCellSelectionTable) {
14318
14505
  const table = this.tableCellSelectionTable;
14319
- if (table.renderedPosition) {
14320
- const localPoint = {
14321
- x: point.x - table.renderedPosition.x,
14322
- y: point.y - table.renderedPosition.y
14323
- };
14324
- const cellAddr = table.getCellAtPoint(localPoint);
14325
- if (cellAddr) {
14326
- // Update selection range
14327
- table.selectRange({
14328
- start: this.tableCellSelectionStart,
14329
- end: cellAddr
14330
- });
14331
- this.render();
14506
+ const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14507
+ // Get the slice for the current page (for multi-page tables)
14508
+ const slice = table.getRenderedSlice(currentPageIndex);
14509
+ const tablePosition = slice?.position || table.renderedPosition;
14510
+ const sliceHeight = slice?.height || table.height;
14511
+ if (tablePosition) {
14512
+ // Check if point is within the table slice on this page
14513
+ const isInsideTable = point.x >= tablePosition.x &&
14514
+ point.x <= tablePosition.x + table.width &&
14515
+ point.y >= tablePosition.y &&
14516
+ point.y <= tablePosition.y + sliceHeight;
14517
+ if (isInsideTable) {
14518
+ const localPoint = {
14519
+ x: point.x - tablePosition.x,
14520
+ y: point.y - tablePosition.y
14521
+ };
14522
+ // If this is a continuation slice, adjust y for the slice offset
14523
+ if (slice && (slice.slicePosition === 'middle' || slice.slicePosition === 'last')) {
14524
+ const headerHeight = slice.headerHeight;
14525
+ if (localPoint.y >= headerHeight) {
14526
+ // Click is in the data rows area - transform coordinates
14527
+ localPoint.y = slice.yOffset + (localPoint.y - headerHeight);
14528
+ }
14529
+ // If y < headerHeight, click is in repeated header - no adjustment needed
14530
+ }
14531
+ const cellAddr = table.getCellAtPoint(localPoint);
14532
+ if (cellAddr) {
14533
+ // Update selection range
14534
+ table.selectRange({
14535
+ start: this.tableCellSelectionStart,
14536
+ end: cellAddr
14537
+ });
14538
+ this.render();
14539
+ }
14332
14540
  }
14333
14541
  }
14334
14542
  e.preventDefault();
@@ -14437,6 +14645,10 @@ class CanvasManager extends EventEmitter {
14437
14645
  });
14438
14646
  }
14439
14647
  }
14648
+ // Re-render to update rendered positions, then update resize handle hit targets
14649
+ // This fixes the bug where resize handles don't work after a resize operation
14650
+ this.render();
14651
+ this.updateResizeHandleHitTargets();
14440
14652
  }
14441
14653
  this.isResizing = false;
14442
14654
  this.dragStart = null;
@@ -14605,13 +14817,21 @@ class CanvasManager extends EventEmitter {
14605
14817
  const hitTestManager = this.flowingTextRenderer.hitTestManager;
14606
14818
  const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
14607
14819
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14608
- // Clicked on embedded object - clear text selection and select it
14820
+ const clickedObject = embeddedObjectHit.data.object;
14821
+ // Check which section the object belongs to
14822
+ const objectSection = this.getSectionForEmbeddedObject(clickedObject);
14823
+ // Only allow selection if object is in the active section
14824
+ if (objectSection && objectSection !== this._activeSection) {
14825
+ // Object is in a different section - ignore the click
14826
+ return;
14827
+ }
14828
+ // Clicked on embedded object in the active section - clear text selection and select it
14609
14829
  const activeFlowingContent = this.getFlowingContentForActiveSection();
14610
14830
  if (activeFlowingContent) {
14611
14831
  activeFlowingContent.clearSelection();
14612
14832
  }
14613
14833
  this.clearSelection();
14614
- this.selectInlineElement(embeddedObjectHit.data.object);
14834
+ this.selectInlineElement(clickedObject);
14615
14835
  return;
14616
14836
  }
14617
14837
  // First check if we clicked on a repeating section indicator
@@ -14851,11 +15071,50 @@ class CanvasManager extends EventEmitter {
14851
15071
  const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
14852
15072
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14853
15073
  const object = embeddedObjectHit.data.object;
14854
- if (object.position === 'relative') {
14855
- canvas.style.cursor = 'move';
14856
- return;
15074
+ // Only show interactive cursors for objects in the active section
15075
+ const objectSection = this.getSectionForEmbeddedObject(object);
15076
+ if (objectSection && objectSection !== this._activeSection) ;
15077
+ else {
15078
+ if (object.position === 'relative') {
15079
+ canvas.style.cursor = 'move';
15080
+ return;
15081
+ }
15082
+ // Show text cursor for text boxes
15083
+ if (object instanceof TextBoxObject) {
15084
+ canvas.style.cursor = 'text';
15085
+ return;
15086
+ }
14857
15087
  }
14858
15088
  }
15089
+ // Check for table cells (show text cursor)
15090
+ const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15091
+ if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15092
+ canvas.style.cursor = 'text';
15093
+ return;
15094
+ }
15095
+ // Check for text regions (body, header, footer - show text cursor)
15096
+ const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15097
+ if (textRegionHit && textRegionHit.data.type === 'text-region') {
15098
+ canvas.style.cursor = 'text';
15099
+ return;
15100
+ }
15101
+ // Also check if point is within any editable region (body, header, footer)
15102
+ // This catches cases where text region hit targets may not cover empty space
15103
+ const bodyRegion = this.regionManager.getBodyRegion();
15104
+ if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15105
+ canvas.style.cursor = 'text';
15106
+ return;
15107
+ }
15108
+ const headerRegion = this.regionManager.getHeaderRegion();
15109
+ if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15110
+ canvas.style.cursor = 'text';
15111
+ return;
15112
+ }
15113
+ const footerRegion = this.regionManager.getFooterRegion();
15114
+ if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15115
+ canvas.style.cursor = 'text';
15116
+ return;
15117
+ }
14859
15118
  canvas.style.cursor = 'default';
14860
15119
  }
14861
15120
  /**
@@ -15032,6 +15291,9 @@ class CanvasManager extends EventEmitter {
15032
15291
  return this.selectedElements.size > 0;
15033
15292
  }
15034
15293
  selectBaseEmbeddedObject(embeddedObject, isPartOfRangeSelection = false) {
15294
+ // Clear focus from any currently focused control (e.g., header/footer text)
15295
+ // This ensures cursor stops blinking in the previous section
15296
+ this.setFocus(null);
15035
15297
  // Mark the embedded object as selected
15036
15298
  const obj = embeddedObject.object || embeddedObject;
15037
15299
  obj.selected = true;
@@ -15498,6 +15760,27 @@ class CanvasManager extends EventEmitter {
15498
15760
  this.emit('section-focus-changed', { section, previousSection });
15499
15761
  }
15500
15762
  }
15763
+ /**
15764
+ * Determine which section (body/header/footer) an embedded object belongs to.
15765
+ */
15766
+ getSectionForEmbeddedObject(object) {
15767
+ const sectionMappings = [
15768
+ { content: this.document.bodyFlowingContent, section: 'body' },
15769
+ { content: this.document.headerFlowingContent, section: 'header' },
15770
+ { content: this.document.footerFlowingContent, section: 'footer' }
15771
+ ];
15772
+ for (const { content, section } of sectionMappings) {
15773
+ if (!content)
15774
+ continue;
15775
+ const embeddedObjects = content.getEmbeddedObjects();
15776
+ for (const [, embeddedObj] of embeddedObjects.entries()) {
15777
+ if (embeddedObj.id === object.id) {
15778
+ return section;
15779
+ }
15780
+ }
15781
+ }
15782
+ return null;
15783
+ }
15501
15784
  /**
15502
15785
  * Detect which section a point is in based on Y coordinate.
15503
15786
  * Uses full page width areas (not just content bounds).
@@ -16111,11 +16394,11 @@ class PDFGenerator {
16111
16394
  width: headerRegion.size.width,
16112
16395
  height: headerRegion.size.height
16113
16396
  };
16114
- await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length);
16397
+ await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.headerHyperlinks);
16115
16398
  }
16116
16399
  // Render body content
16117
16400
  if (flowedContent?.body && flowedContent.body[pageIndex]) {
16118
- await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length);
16401
+ await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.bodyHyperlinks);
16119
16402
  }
16120
16403
  // Render footer if present
16121
16404
  if (flowedContent?.footer && document.footerFlowingContent) {
@@ -16126,7 +16409,7 @@ class PDFGenerator {
16126
16409
  width: footerRegion.size.width,
16127
16410
  height: footerRegion.size.height
16128
16411
  };
16129
- await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length);
16412
+ await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.footerHyperlinks);
16130
16413
  }
16131
16414
  }
16132
16415
  catch (pageError) {
@@ -16211,10 +16494,12 @@ class PDFGenerator {
16211
16494
  /**
16212
16495
  * Render a flowed page to PDF.
16213
16496
  */
16214
- async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages) {
16497
+ async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages, hyperlinks) {
16215
16498
  let y = bounds.y;
16216
16499
  // Track relative objects to render after all lines (so they appear on top)
16217
16500
  const relativeObjects = [];
16501
+ // Track rendered text positions for hyperlink annotations
16502
+ const renderedRuns = [];
16218
16503
  for (const line of flowedPage.lines) {
16219
16504
  // Collect relative objects from this line
16220
16505
  if (line.embeddedObjects) {
@@ -16245,10 +16530,10 @@ class PDFGenerator {
16245
16530
  const font = this.getFont(formatting);
16246
16531
  const fontSize = formatting.fontSize || 14;
16247
16532
  const color = parseColor(formatting.color || '#000000');
16533
+ const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16248
16534
  // Draw background if present
16249
16535
  if (formatting.backgroundColor) {
16250
16536
  const bgColor = parseColor(formatting.backgroundColor);
16251
- const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16252
16537
  drawFilledRect(pdfPage, runX, y, textWidth, line.height, bgColor, pageHeight);
16253
16538
  }
16254
16539
  // Draw text - position at baseline
@@ -16260,8 +16545,19 @@ class PDFGenerator {
16260
16545
  size: fontSize,
16261
16546
  color
16262
16547
  });
16548
+ // Track this run's position for hyperlink annotations
16549
+ if (hyperlinks && hyperlinks.length > 0) {
16550
+ renderedRuns.push({
16551
+ x: runX,
16552
+ y: y,
16553
+ width: textWidth,
16554
+ height: line.height,
16555
+ startIndex: run.startIndex,
16556
+ endIndex: run.endIndex
16557
+ });
16558
+ }
16263
16559
  // Advance X position
16264
- runX += font.widthOfTextAtSize(safeText, fontSize);
16560
+ runX += textWidth;
16265
16561
  // Add extra word spacing for justified text
16266
16562
  if (line.extraWordSpacing && safeText.includes(' ')) {
16267
16563
  const spaceCount = (safeText.match(/ /g) || []).length;
@@ -16299,6 +16595,62 @@ class PDFGenerator {
16299
16595
  }
16300
16596
  // Render relative objects last (so they appear on top of text)
16301
16597
  await this.renderRelativeObjects(pdfPage, relativeObjects, pageHeight, pageIndex, totalPages);
16598
+ // Create hyperlink annotations
16599
+ if (hyperlinks && hyperlinks.length > 0 && renderedRuns.length > 0) {
16600
+ this.createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight);
16601
+ }
16602
+ }
16603
+ /**
16604
+ * Create PDF link annotations for hyperlinks.
16605
+ * Matches hyperlink ranges with rendered text positions and creates clickable links.
16606
+ */
16607
+ createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight) {
16608
+ for (const hyperlink of hyperlinks) {
16609
+ // Find all runs that overlap with this hyperlink
16610
+ const overlappingRuns = renderedRuns.filter(run => run.startIndex < hyperlink.endIndex && run.endIndex > hyperlink.startIndex);
16611
+ if (overlappingRuns.length === 0)
16612
+ continue;
16613
+ // Calculate bounding box for the hyperlink
16614
+ // For multi-line hyperlinks, we create separate annotations for each line segment
16615
+ // Group runs by their Y position (same line)
16616
+ const runsByLine = new Map();
16617
+ for (const run of overlappingRuns) {
16618
+ const key = run.y;
16619
+ if (!runsByLine.has(key)) {
16620
+ runsByLine.set(key, []);
16621
+ }
16622
+ runsByLine.get(key).push(run);
16623
+ }
16624
+ // Create an annotation for each line segment
16625
+ for (const [lineY, lineRuns] of runsByLine) {
16626
+ // Calculate bounds for this line's portion of the hyperlink
16627
+ const minX = Math.min(...lineRuns.map(r => r.x));
16628
+ const maxX = Math.max(...lineRuns.map(r => r.x + r.width));
16629
+ const height = lineRuns[0].height;
16630
+ // Transform to PDF coordinates (Y is inverted)
16631
+ const pdfY = pageHeight - lineY - height;
16632
+ // Create link annotation using pdf-lib
16633
+ const linkAnnotation = pdfPage.doc.context.obj({
16634
+ Type: 'Annot',
16635
+ Subtype: 'Link',
16636
+ Rect: [minX, pdfY, maxX, pdfY + height],
16637
+ Border: [0, 0, 0], // No visible border
16638
+ A: {
16639
+ Type: 'Action',
16640
+ S: 'URI',
16641
+ URI: hyperlink.url
16642
+ }
16643
+ });
16644
+ // Add annotation to page
16645
+ const annotations = pdfPage.node.get(pdfPage.doc.context.obj('Annots'));
16646
+ if (annotations) {
16647
+ annotations.push(linkAnnotation);
16648
+ }
16649
+ else {
16650
+ pdfPage.node.set(pdfPage.doc.context.obj('Annots'), pdfPage.doc.context.obj([linkAnnotation]));
16651
+ }
16652
+ }
16653
+ }
16302
16654
  }
16303
16655
  /**
16304
16656
  * Calculate alignment offset for a line.
@@ -18522,11 +18874,11 @@ class MutationUndo {
18522
18874
  const data = mutation.data;
18523
18875
  // Restore deleted text
18524
18876
  content.insertTextAt(data.position, data.deletedText);
18525
- // Restore formatting
18877
+ // Restore formatting using setFormattingAt to replace completely
18526
18878
  if (data.deletedFormatting) {
18527
18879
  const fm = content.getFormattingManager();
18528
18880
  data.deletedFormatting.forEach((style, offset) => {
18529
- fm.applyFormatting(data.position + offset, data.position + offset + 1, style);
18881
+ fm.setFormattingAt(data.position + offset, style, true);
18530
18882
  });
18531
18883
  }
18532
18884
  // Restore substitution fields
@@ -18552,9 +18904,10 @@ class MutationUndo {
18552
18904
  return;
18553
18905
  const data = mutation.data;
18554
18906
  const fm = content.getFormattingManager();
18555
- // Restore previous formatting
18907
+ // Restore previous formatting using setFormattingAt to replace completely
18908
+ // (not merge, which would leave properties like backgroundColor intact)
18556
18909
  data.previousFormatting.forEach((style, offset) => {
18557
- fm.applyFormatting(data.start + offset, data.start + offset + 1, style);
18910
+ fm.setFormattingAt(data.start + offset, style, true);
18558
18911
  });
18559
18912
  }
18560
18913
  redoFormat(mutation) {
@@ -20924,8 +21277,12 @@ class PCEditor extends EventEmitter {
20924
21277
  }
20925
21278
  });
20926
21279
  this.document.on('settings-changed', () => {
20927
- // When settings change (like margins), we need to check if pages need to be added/removed
21280
+ // When settings change (like page size, orientation, margins), we need to:
21281
+ // 1. Update canvas sizes to match new page dimensions
21282
+ // 2. Re-render the content
21283
+ // 3. Check if pages need to be added/removed
20928
21284
  if (this.canvasManager) {
21285
+ this.canvasManager.updateCanvasSizes();
20929
21286
  this.canvasManager.render();
20930
21287
  // Defer the page check to allow reflow to complete
20931
21288
  setTimeout(() => {
@@ -22767,6 +23124,144 @@ class PCEditor extends EventEmitter {
22767
23124
  return null;
22768
23125
  }
22769
23126
  // ============================================
23127
+ // Text Box Update Operations
23128
+ // ============================================
23129
+ /**
23130
+ * Update properties of a text box.
23131
+ * @param textBoxId The ID of the text box to update
23132
+ * @param updates The properties to update
23133
+ */
23134
+ updateTextBox(textBoxId, updates) {
23135
+ if (!this._isReady)
23136
+ return false;
23137
+ // Find the text box in all flowing contents
23138
+ const textBox = this.findTextBoxById(textBoxId);
23139
+ if (!textBox) {
23140
+ console.warn(`[PCEditor.updateTextBox] Text box not found: ${textBoxId}`);
23141
+ return false;
23142
+ }
23143
+ // Apply updates
23144
+ if (updates.position !== undefined) {
23145
+ textBox.position = updates.position;
23146
+ }
23147
+ if (updates.relativeOffset !== undefined) {
23148
+ textBox.relativeOffset = updates.relativeOffset;
23149
+ }
23150
+ if (updates.backgroundColor !== undefined) {
23151
+ textBox.backgroundColor = updates.backgroundColor;
23152
+ }
23153
+ if (updates.border !== undefined) {
23154
+ // Merge with existing border
23155
+ const existingBorder = textBox.border;
23156
+ textBox.border = {
23157
+ top: updates.border.top || existingBorder.top,
23158
+ right: updates.border.right || existingBorder.right,
23159
+ bottom: updates.border.bottom || existingBorder.bottom,
23160
+ left: updates.border.left || existingBorder.left
23161
+ };
23162
+ }
23163
+ if (updates.padding !== undefined) {
23164
+ textBox.padding = updates.padding;
23165
+ }
23166
+ this.render();
23167
+ this.emit('textbox-updated', { textBoxId, updates });
23168
+ return true;
23169
+ }
23170
+ /**
23171
+ * Find a text box by ID across all flowing contents.
23172
+ */
23173
+ findTextBoxById(textBoxId) {
23174
+ const flowingContents = [
23175
+ this.document.bodyFlowingContent,
23176
+ this.document.headerFlowingContent,
23177
+ this.document.footerFlowingContent
23178
+ ].filter(Boolean);
23179
+ for (const flowingContent of flowingContents) {
23180
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23181
+ for (const [, obj] of embeddedObjects.entries()) {
23182
+ if (obj.id === textBoxId && obj instanceof TextBoxObject) {
23183
+ return obj;
23184
+ }
23185
+ }
23186
+ }
23187
+ return null;
23188
+ }
23189
+ // ============================================
23190
+ // Image Update Operations
23191
+ // ============================================
23192
+ /**
23193
+ * Update properties of an image.
23194
+ * @param imageId The ID of the image to update
23195
+ * @param updates The properties to update
23196
+ */
23197
+ updateImage(imageId, updates) {
23198
+ if (!this._isReady)
23199
+ return false;
23200
+ // Find the image in all flowing contents
23201
+ const image = this.findImageById(imageId);
23202
+ if (!image) {
23203
+ console.warn(`[PCEditor.updateImage] Image not found: ${imageId}`);
23204
+ return false;
23205
+ }
23206
+ // Apply updates
23207
+ if (updates.position !== undefined) {
23208
+ image.position = updates.position;
23209
+ }
23210
+ if (updates.relativeOffset !== undefined) {
23211
+ image.relativeOffset = updates.relativeOffset;
23212
+ }
23213
+ if (updates.fit !== undefined) {
23214
+ image.fit = updates.fit;
23215
+ }
23216
+ if (updates.resizeMode !== undefined) {
23217
+ image.resizeMode = updates.resizeMode;
23218
+ }
23219
+ if (updates.alt !== undefined) {
23220
+ image.alt = updates.alt;
23221
+ }
23222
+ this.render();
23223
+ this.emit('image-updated', { imageId, updates });
23224
+ return true;
23225
+ }
23226
+ /**
23227
+ * Set the source of an image.
23228
+ * @param imageId The ID of the image
23229
+ * @param dataUrl The data URL of the new image source
23230
+ * @param options Optional sizing options
23231
+ */
23232
+ setImageSource(imageId, dataUrl, options) {
23233
+ if (!this._isReady)
23234
+ return false;
23235
+ const image = this.findImageById(imageId);
23236
+ if (!image) {
23237
+ console.warn(`[PCEditor.setImageSource] Image not found: ${imageId}`);
23238
+ return false;
23239
+ }
23240
+ image.setSource(dataUrl, options);
23241
+ this.render();
23242
+ this.emit('image-source-changed', { imageId });
23243
+ return true;
23244
+ }
23245
+ /**
23246
+ * Find an image by ID across all flowing contents.
23247
+ */
23248
+ findImageById(imageId) {
23249
+ const flowingContents = [
23250
+ this.document.bodyFlowingContent,
23251
+ this.document.headerFlowingContent,
23252
+ this.document.footerFlowingContent
23253
+ ].filter(Boolean);
23254
+ for (const flowingContent of flowingContents) {
23255
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
23256
+ for (const [, obj] of embeddedObjects.entries()) {
23257
+ if (obj.id === imageId && obj instanceof ImageObject) {
23258
+ return obj;
23259
+ }
23260
+ }
23261
+ }
23262
+ return null;
23263
+ }
23264
+ // ============================================
22770
23265
  // Table Structure Operations (with undo support)
22771
23266
  // ============================================
22772
23267
  /**
@@ -24837,8 +25332,2628 @@ class VerticalRuler extends RulerControl {
24837
25332
  }
24838
25333
  }
24839
25334
 
25335
+ /**
25336
+ * BasePane - Abstract base class for editor property panes.
25337
+ *
25338
+ * Panes are property editors that work with PCEditor via the public API only.
25339
+ * They are content-only (no title bar) for flexible layout by consumers.
25340
+ */
25341
+ /**
25342
+ * Abstract base class for editor panes.
25343
+ */
25344
+ class BasePane extends BaseControl {
25345
+ constructor(id, options = {}) {
25346
+ super(id, options);
25347
+ this.sectionElement = null;
25348
+ this.className = options.className || '';
25349
+ }
25350
+ /**
25351
+ * Attach the pane to an editor.
25352
+ */
25353
+ attach(options) {
25354
+ // Store the section element if provided
25355
+ this.sectionElement = options.sectionElement || null;
25356
+ super.attach(options);
25357
+ }
25358
+ /**
25359
+ * Show the pane (and section element if provided).
25360
+ */
25361
+ show() {
25362
+ this._isVisible = true;
25363
+ if (this.sectionElement) {
25364
+ this.sectionElement.style.display = '';
25365
+ }
25366
+ if (this.element) {
25367
+ this.element.style.display = '';
25368
+ this.update();
25369
+ }
25370
+ this.emit('visibility-changed', { visible: true });
25371
+ }
25372
+ /**
25373
+ * Hide the pane (and section element if provided).
25374
+ */
25375
+ hide() {
25376
+ this._isVisible = false;
25377
+ if (this.sectionElement) {
25378
+ this.sectionElement.style.display = 'none';
25379
+ }
25380
+ if (this.element) {
25381
+ this.element.style.display = 'none';
25382
+ }
25383
+ this.emit('visibility-changed', { visible: false });
25384
+ }
25385
+ /**
25386
+ * Create a form group element with label.
25387
+ */
25388
+ createFormGroup(label, inputElement, options) {
25389
+ const group = document.createElement('div');
25390
+ group.className = 'pc-pane-form-group';
25391
+ if (options?.inline) {
25392
+ group.classList.add('pc-pane-form-group--inline');
25393
+ }
25394
+ const labelEl = document.createElement('label');
25395
+ labelEl.className = 'pc-pane-label';
25396
+ labelEl.textContent = label;
25397
+ group.appendChild(labelEl);
25398
+ group.appendChild(inputElement);
25399
+ if (options?.hint) {
25400
+ const hintEl = document.createElement('span');
25401
+ hintEl.className = 'pc-pane-hint';
25402
+ hintEl.textContent = options.hint;
25403
+ group.appendChild(hintEl);
25404
+ }
25405
+ return group;
25406
+ }
25407
+ /**
25408
+ * Create a text input element.
25409
+ */
25410
+ createTextInput(options) {
25411
+ const input = document.createElement('input');
25412
+ input.type = options?.type || 'text';
25413
+ input.className = 'pc-pane-input';
25414
+ if (options?.placeholder) {
25415
+ input.placeholder = options.placeholder;
25416
+ }
25417
+ if (options?.value !== undefined) {
25418
+ input.value = options.value;
25419
+ }
25420
+ return input;
25421
+ }
25422
+ /**
25423
+ * Create a number input element.
25424
+ */
25425
+ createNumberInput(options) {
25426
+ const input = document.createElement('input');
25427
+ input.type = 'number';
25428
+ input.className = 'pc-pane-input pc-pane-input--number';
25429
+ if (options?.min !== undefined)
25430
+ input.min = String(options.min);
25431
+ if (options?.max !== undefined)
25432
+ input.max = String(options.max);
25433
+ if (options?.step !== undefined)
25434
+ input.step = String(options.step);
25435
+ if (options?.value !== undefined)
25436
+ input.value = String(options.value);
25437
+ return input;
25438
+ }
25439
+ /**
25440
+ * Create a select element with options.
25441
+ */
25442
+ createSelect(optionsList, selectedValue) {
25443
+ const select = document.createElement('select');
25444
+ select.className = 'pc-pane-select';
25445
+ for (const opt of optionsList) {
25446
+ const option = document.createElement('option');
25447
+ option.value = opt.value;
25448
+ option.textContent = opt.label;
25449
+ if (opt.value === selectedValue) {
25450
+ option.selected = true;
25451
+ }
25452
+ select.appendChild(option);
25453
+ }
25454
+ return select;
25455
+ }
25456
+ /**
25457
+ * Create a color input element.
25458
+ */
25459
+ createColorInput(value) {
25460
+ const input = document.createElement('input');
25461
+ input.type = 'color';
25462
+ input.className = 'pc-pane-color';
25463
+ if (value) {
25464
+ input.value = value;
25465
+ }
25466
+ return input;
25467
+ }
25468
+ /**
25469
+ * Create a checkbox element.
25470
+ */
25471
+ createCheckbox(label, checked) {
25472
+ const wrapper = document.createElement('label');
25473
+ wrapper.className = 'pc-pane-checkbox';
25474
+ const input = document.createElement('input');
25475
+ input.type = 'checkbox';
25476
+ if (checked) {
25477
+ input.checked = true;
25478
+ }
25479
+ const span = document.createElement('span');
25480
+ span.textContent = label;
25481
+ wrapper.appendChild(input);
25482
+ wrapper.appendChild(span);
25483
+ return wrapper;
25484
+ }
25485
+ /**
25486
+ * Create a button element.
25487
+ */
25488
+ createButton(label, options) {
25489
+ const button = document.createElement('button');
25490
+ button.type = 'button';
25491
+ button.className = 'pc-pane-button';
25492
+ if (options?.variant) {
25493
+ button.classList.add(`pc-pane-button--${options.variant}`);
25494
+ }
25495
+ button.textContent = label;
25496
+ return button;
25497
+ }
25498
+ /**
25499
+ * Create a button group container.
25500
+ */
25501
+ createButtonGroup() {
25502
+ const group = document.createElement('div');
25503
+ group.className = 'pc-pane-button-group';
25504
+ return group;
25505
+ }
25506
+ /**
25507
+ * Create a section divider with optional label.
25508
+ */
25509
+ createSection(label) {
25510
+ const section = document.createElement('div');
25511
+ section.className = 'pc-pane-section';
25512
+ if (label) {
25513
+ const labelEl = document.createElement('div');
25514
+ labelEl.className = 'pc-pane-section-label';
25515
+ labelEl.textContent = label;
25516
+ section.appendChild(labelEl);
25517
+ }
25518
+ return section;
25519
+ }
25520
+ /**
25521
+ * Create a row container for inline elements.
25522
+ */
25523
+ createRow() {
25524
+ const row = document.createElement('div');
25525
+ row.className = 'pc-pane-row';
25526
+ return row;
25527
+ }
25528
+ /**
25529
+ * Create a hint/info text element.
25530
+ */
25531
+ createHint(text) {
25532
+ const hint = document.createElement('div');
25533
+ hint.className = 'pc-pane-hint';
25534
+ hint.textContent = text;
25535
+ return hint;
25536
+ }
25537
+ /**
25538
+ * Add immediate apply listener for text inputs (blur + Enter).
25539
+ */
25540
+ addImmediateApplyListener(element, handler) {
25541
+ const apply = () => {
25542
+ handler(element.value);
25543
+ };
25544
+ // Selects and color inputs: apply on change
25545
+ if (element instanceof HTMLSelectElement ||
25546
+ (element instanceof HTMLInputElement && element.type === 'color')) {
25547
+ element.addEventListener('change', apply);
25548
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25549
+ }
25550
+ else {
25551
+ // Text/number inputs: apply on blur or Enter
25552
+ element.addEventListener('blur', apply);
25553
+ const keyHandler = (e) => {
25554
+ if (e.key === 'Enter') {
25555
+ e.preventDefault();
25556
+ apply();
25557
+ }
25558
+ };
25559
+ element.addEventListener('keydown', keyHandler);
25560
+ this.eventCleanup.push(() => {
25561
+ element.removeEventListener('blur', apply);
25562
+ element.removeEventListener('keydown', keyHandler);
25563
+ });
25564
+ }
25565
+ }
25566
+ /**
25567
+ * Add immediate apply listener for checkbox inputs.
25568
+ */
25569
+ addCheckboxListener(element, handler) {
25570
+ const apply = () => handler(element.checked);
25571
+ element.addEventListener('change', apply);
25572
+ this.eventCleanup.push(() => element.removeEventListener('change', apply));
25573
+ }
25574
+ /**
25575
+ * Add button click handler with focus steal prevention.
25576
+ */
25577
+ addButtonListener(button, handler) {
25578
+ // Prevent focus steal on mousedown
25579
+ const preventFocus = (e) => {
25580
+ e.preventDefault();
25581
+ this.saveEditorContext();
25582
+ };
25583
+ button.addEventListener('mousedown', preventFocus);
25584
+ button.addEventListener('click', handler);
25585
+ this.eventCleanup.push(() => {
25586
+ button.removeEventListener('mousedown', preventFocus);
25587
+ button.removeEventListener('click', handler);
25588
+ });
25589
+ }
25590
+ /**
25591
+ * Save editor context before UI elements steal focus.
25592
+ */
25593
+ saveEditorContext() {
25594
+ if (this.editor) {
25595
+ this.editor.saveEditingContext();
25596
+ }
25597
+ }
25598
+ /**
25599
+ * Final createElement that wraps content in pane structure.
25600
+ * Content-only, no title bar.
25601
+ */
25602
+ createElement() {
25603
+ const wrapper = document.createElement('div');
25604
+ wrapper.className = 'pc-pane';
25605
+ if (this.className) {
25606
+ wrapper.classList.add(this.className);
25607
+ }
25608
+ wrapper.setAttribute('data-pane-id', this.id);
25609
+ const content = this.createContent();
25610
+ wrapper.appendChild(content);
25611
+ return wrapper;
25612
+ }
25613
+ }
25614
+
25615
+ /**
25616
+ * DocumentInfoPane - Read-only document information display.
25617
+ *
25618
+ * Shows:
25619
+ * - Page count
25620
+ * - Page size
25621
+ * - Page orientation
25622
+ */
25623
+ class DocumentInfoPane extends BasePane {
25624
+ constructor(id = 'document-info') {
25625
+ super(id, { className: 'pc-pane-document-info' });
25626
+ this.pageCountEl = null;
25627
+ this.pageSizeEl = null;
25628
+ this.pageOrientationEl = null;
25629
+ }
25630
+ attach(options) {
25631
+ super.attach(options);
25632
+ // Subscribe to document changes
25633
+ if (this.editor) {
25634
+ const updateHandler = () => this.update();
25635
+ this.editor.on('document-changed', updateHandler);
25636
+ this.editor.on('page-added', updateHandler);
25637
+ this.editor.on('page-removed', updateHandler);
25638
+ this.eventCleanup.push(() => {
25639
+ this.editor?.off('document-changed', updateHandler);
25640
+ this.editor?.off('page-added', updateHandler);
25641
+ this.editor?.off('page-removed', updateHandler);
25642
+ });
25643
+ // Initial update
25644
+ this.update();
25645
+ }
25646
+ }
25647
+ createContent() {
25648
+ const container = document.createElement('div');
25649
+ container.className = 'pc-pane-info-list';
25650
+ // Page count
25651
+ const countRow = this.createInfoRow('Pages', '0');
25652
+ this.pageCountEl = countRow.querySelector('.pc-pane-info-value');
25653
+ container.appendChild(countRow);
25654
+ // Page size
25655
+ const sizeRow = this.createInfoRow('Size', '-');
25656
+ this.pageSizeEl = sizeRow.querySelector('.pc-pane-info-value');
25657
+ container.appendChild(sizeRow);
25658
+ // Page orientation
25659
+ const orientationRow = this.createInfoRow('Orientation', '-');
25660
+ this.pageOrientationEl = orientationRow.querySelector('.pc-pane-info-value');
25661
+ container.appendChild(orientationRow);
25662
+ return container;
25663
+ }
25664
+ createInfoRow(label, value) {
25665
+ const row = document.createElement('div');
25666
+ row.className = 'pc-pane-info';
25667
+ const labelEl = document.createElement('span');
25668
+ labelEl.className = 'pc-pane-info-label';
25669
+ labelEl.textContent = label;
25670
+ const valueEl = document.createElement('span');
25671
+ valueEl.className = 'pc-pane-info-value';
25672
+ valueEl.textContent = value;
25673
+ row.appendChild(labelEl);
25674
+ row.appendChild(valueEl);
25675
+ return row;
25676
+ }
25677
+ /**
25678
+ * Update the displayed information from the editor.
25679
+ */
25680
+ update() {
25681
+ if (!this.editor)
25682
+ return;
25683
+ const doc = this.editor.getDocument();
25684
+ if (this.pageCountEl) {
25685
+ this.pageCountEl.textContent = doc.pages.length.toString();
25686
+ }
25687
+ if (this.pageSizeEl && doc.settings) {
25688
+ this.pageSizeEl.textContent = doc.settings.pageSize;
25689
+ }
25690
+ if (this.pageOrientationEl && doc.settings) {
25691
+ const orientation = doc.settings.pageOrientation;
25692
+ this.pageOrientationEl.textContent =
25693
+ orientation.charAt(0).toUpperCase() + orientation.slice(1);
25694
+ }
25695
+ }
25696
+ }
25697
+
25698
+ /**
25699
+ * ViewSettingsPane - Toggle buttons for view options.
25700
+ *
25701
+ * Toggles:
25702
+ * - Rulers (requires external callback since rulers are optional controls)
25703
+ * - Control characters
25704
+ * - Margin lines
25705
+ * - Grid
25706
+ */
25707
+ class ViewSettingsPane extends BasePane {
25708
+ constructor(id = 'view-settings', options = {}) {
25709
+ super(id, { className: 'pc-pane-view-settings', ...options });
25710
+ this.rulersBtn = null;
25711
+ this.controlCharsBtn = null;
25712
+ this.marginLinesBtn = null;
25713
+ this.gridBtn = null;
25714
+ this.onToggleRulers = options.onToggleRulers;
25715
+ this.rulersVisible = options.rulersVisible ?? true;
25716
+ }
25717
+ attach(options) {
25718
+ super.attach(options);
25719
+ // Subscribe to editor events
25720
+ if (this.editor) {
25721
+ const updateHandler = () => this.updateButtonStates();
25722
+ this.editor.on('grid-changed', updateHandler);
25723
+ this.editor.on('margin-lines-changed', updateHandler);
25724
+ this.editor.on('control-characters-changed', updateHandler);
25725
+ this.eventCleanup.push(() => {
25726
+ this.editor?.off('grid-changed', updateHandler);
25727
+ this.editor?.off('margin-lines-changed', updateHandler);
25728
+ this.editor?.off('control-characters-changed', updateHandler);
25729
+ });
25730
+ // Initial state
25731
+ this.updateButtonStates();
25732
+ }
25733
+ }
25734
+ createContent() {
25735
+ const container = document.createElement('div');
25736
+ container.className = 'pc-pane-button-group pc-pane-view-toggles';
25737
+ // Rulers toggle (only if callback provided)
25738
+ if (this.onToggleRulers) {
25739
+ this.rulersBtn = this.createToggleButton('Rulers', this.rulersVisible);
25740
+ this.addButtonListener(this.rulersBtn, () => this.toggleRulers());
25741
+ container.appendChild(this.rulersBtn);
25742
+ }
25743
+ // Control characters toggle
25744
+ this.controlCharsBtn = this.createToggleButton('Control Chars', false);
25745
+ this.addButtonListener(this.controlCharsBtn, () => this.toggleControlChars());
25746
+ container.appendChild(this.controlCharsBtn);
25747
+ // Margin lines toggle
25748
+ this.marginLinesBtn = this.createToggleButton('Margin Lines', true);
25749
+ this.addButtonListener(this.marginLinesBtn, () => this.toggleMarginLines());
25750
+ container.appendChild(this.marginLinesBtn);
25751
+ // Grid toggle
25752
+ this.gridBtn = this.createToggleButton('Grid', true);
25753
+ this.addButtonListener(this.gridBtn, () => this.toggleGrid());
25754
+ container.appendChild(this.gridBtn);
25755
+ return container;
25756
+ }
25757
+ createToggleButton(label, active) {
25758
+ const button = document.createElement('button');
25759
+ button.type = 'button';
25760
+ button.className = 'pc-pane-toggle';
25761
+ if (active) {
25762
+ button.classList.add('pc-pane-toggle--active');
25763
+ }
25764
+ button.textContent = label;
25765
+ button.title = `Toggle ${label}`;
25766
+ return button;
25767
+ }
25768
+ toggleRulers() {
25769
+ if (this.onToggleRulers) {
25770
+ this.onToggleRulers();
25771
+ this.rulersVisible = !this.rulersVisible;
25772
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', this.rulersVisible);
25773
+ }
25774
+ }
25775
+ toggleControlChars() {
25776
+ if (!this.editor)
25777
+ return;
25778
+ const current = this.editor.getShowControlCharacters();
25779
+ this.editor.setShowControlCharacters(!current);
25780
+ }
25781
+ toggleMarginLines() {
25782
+ if (!this.editor)
25783
+ return;
25784
+ const current = this.editor.getShowMarginLines();
25785
+ this.editor.setShowMarginLines(!current);
25786
+ }
25787
+ toggleGrid() {
25788
+ if (!this.editor)
25789
+ return;
25790
+ const current = this.editor.getShowGrid();
25791
+ this.editor.setShowGrid(!current);
25792
+ }
25793
+ updateButtonStates() {
25794
+ if (!this.editor)
25795
+ return;
25796
+ if (this.controlCharsBtn) {
25797
+ this.controlCharsBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowControlCharacters());
25798
+ }
25799
+ if (this.marginLinesBtn) {
25800
+ this.marginLinesBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowMarginLines());
25801
+ }
25802
+ if (this.gridBtn) {
25803
+ this.gridBtn.classList.toggle('pc-pane-toggle--active', this.editor.getShowGrid());
25804
+ }
25805
+ }
25806
+ /**
25807
+ * Update ruler button state externally (since rulers are external controls).
25808
+ */
25809
+ setRulersVisible(visible) {
25810
+ this.rulersVisible = visible;
25811
+ this.rulersBtn?.classList.toggle('pc-pane-toggle--active', visible);
25812
+ }
25813
+ /**
25814
+ * Update the pane from current editor state.
25815
+ */
25816
+ update() {
25817
+ this.updateButtonStates();
25818
+ }
25819
+ }
25820
+
25821
+ /**
25822
+ * DocumentSettingsPane - Edit margins, page size, and orientation.
25823
+ *
25824
+ * Uses the PCEditor public API:
25825
+ * - editor.getDocumentSettings()
25826
+ * - editor.updateDocumentSettings()
25827
+ */
25828
+ class DocumentSettingsPane extends BasePane {
25829
+ constructor(id = 'document-settings') {
25830
+ super(id, { className: 'pc-pane-document-settings' });
25831
+ this.marginTopInput = null;
25832
+ this.marginRightInput = null;
25833
+ this.marginBottomInput = null;
25834
+ this.marginLeftInput = null;
25835
+ this.pageSizeSelect = null;
25836
+ this.orientationSelect = null;
25837
+ }
25838
+ attach(options) {
25839
+ super.attach(options);
25840
+ // Load current settings
25841
+ if (this.editor) {
25842
+ this.loadSettings();
25843
+ // Subscribe to document changes
25844
+ const updateHandler = () => this.loadSettings();
25845
+ this.editor.on('document-changed', updateHandler);
25846
+ this.eventCleanup.push(() => {
25847
+ this.editor?.off('document-changed', updateHandler);
25848
+ });
25849
+ }
25850
+ }
25851
+ createContent() {
25852
+ const container = document.createElement('div');
25853
+ // Margins section
25854
+ const marginsSection = this.createSection('Margins (mm)');
25855
+ const marginsGrid = document.createElement('div');
25856
+ marginsGrid.className = 'pc-pane-margins-grid';
25857
+ this.marginTopInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25858
+ this.marginRightInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25859
+ this.marginBottomInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25860
+ this.marginLeftInput = this.createNumberInput({ min: 5, max: 50, step: 0.5, value: 20 });
25861
+ marginsGrid.appendChild(this.createFormGroup('Top', this.marginTopInput, { inline: true }));
25862
+ marginsGrid.appendChild(this.createFormGroup('Right', this.marginRightInput, { inline: true }));
25863
+ marginsGrid.appendChild(this.createFormGroup('Bottom', this.marginBottomInput, { inline: true }));
25864
+ marginsGrid.appendChild(this.createFormGroup('Left', this.marginLeftInput, { inline: true }));
25865
+ marginsSection.appendChild(marginsGrid);
25866
+ // Apply margins button
25867
+ const applyMarginsBtn = this.createButton('Apply Margins');
25868
+ this.addButtonListener(applyMarginsBtn, () => this.applyMargins());
25869
+ marginsSection.appendChild(applyMarginsBtn);
25870
+ container.appendChild(marginsSection);
25871
+ // Page size section
25872
+ const pageSizeSection = this.createSection();
25873
+ this.pageSizeSelect = this.createSelect([
25874
+ { value: 'A4', label: 'A4' },
25875
+ { value: 'Letter', label: 'Letter' },
25876
+ { value: 'Legal', label: 'Legal' },
25877
+ { value: 'A3', label: 'A3' }
25878
+ ], 'A4');
25879
+ this.addImmediateApplyListener(this.pageSizeSelect, () => this.applyPageSettings());
25880
+ pageSizeSection.appendChild(this.createFormGroup('Page Size', this.pageSizeSelect));
25881
+ container.appendChild(pageSizeSection);
25882
+ // Orientation section
25883
+ const orientationSection = this.createSection();
25884
+ this.orientationSelect = this.createSelect([
25885
+ { value: 'portrait', label: 'Portrait' },
25886
+ { value: 'landscape', label: 'Landscape' }
25887
+ ], 'portrait');
25888
+ this.addImmediateApplyListener(this.orientationSelect, () => this.applyPageSettings());
25889
+ orientationSection.appendChild(this.createFormGroup('Orientation', this.orientationSelect));
25890
+ container.appendChild(orientationSection);
25891
+ return container;
25892
+ }
25893
+ loadSettings() {
25894
+ if (!this.editor)
25895
+ return;
25896
+ try {
25897
+ const settings = this.editor.getDocumentSettings();
25898
+ if (this.marginTopInput) {
25899
+ this.marginTopInput.value = settings.margins.top.toString();
25900
+ }
25901
+ if (this.marginRightInput) {
25902
+ this.marginRightInput.value = settings.margins.right.toString();
25903
+ }
25904
+ if (this.marginBottomInput) {
25905
+ this.marginBottomInput.value = settings.margins.bottom.toString();
25906
+ }
25907
+ if (this.marginLeftInput) {
25908
+ this.marginLeftInput.value = settings.margins.left.toString();
25909
+ }
25910
+ if (this.pageSizeSelect) {
25911
+ this.pageSizeSelect.value = settings.pageSize;
25912
+ }
25913
+ if (this.orientationSelect) {
25914
+ this.orientationSelect.value = settings.pageOrientation;
25915
+ }
25916
+ }
25917
+ catch (error) {
25918
+ console.error('Failed to load document settings:', error);
25919
+ }
25920
+ }
25921
+ applyMargins() {
25922
+ if (!this.editor)
25923
+ return;
25924
+ const margins = {
25925
+ top: parseFloat(this.marginTopInput?.value || '20'),
25926
+ right: parseFloat(this.marginRightInput?.value || '20'),
25927
+ bottom: parseFloat(this.marginBottomInput?.value || '20'),
25928
+ left: parseFloat(this.marginLeftInput?.value || '20')
25929
+ };
25930
+ try {
25931
+ this.editor.updateDocumentSettings({ margins });
25932
+ }
25933
+ catch (error) {
25934
+ console.error('Failed to update margins:', error);
25935
+ }
25936
+ }
25937
+ applyPageSettings() {
25938
+ if (!this.editor)
25939
+ return;
25940
+ const settings = {};
25941
+ if (this.pageSizeSelect) {
25942
+ settings.pageSize = this.pageSizeSelect.value;
25943
+ }
25944
+ if (this.orientationSelect) {
25945
+ settings.pageOrientation = this.orientationSelect.value;
25946
+ }
25947
+ try {
25948
+ this.editor.updateDocumentSettings(settings);
25949
+ }
25950
+ catch (error) {
25951
+ console.error('Failed to update page settings:', error);
25952
+ }
25953
+ }
25954
+ /**
25955
+ * Update the pane from current editor state.
25956
+ */
25957
+ update() {
25958
+ this.loadSettings();
25959
+ }
25960
+ }
25961
+
25962
+ /**
25963
+ * MergeDataPane - JSON data input for mail merge/substitution.
25964
+ *
25965
+ * Uses the PCEditor public API:
25966
+ * - editor.applyMergeData()
25967
+ */
25968
+ class MergeDataPane extends BasePane {
25969
+ constructor(id = 'merge-data', options = {}) {
25970
+ super(id, { className: 'pc-pane-merge-data', ...options });
25971
+ this.textarea = null;
25972
+ this.errorHint = null;
25973
+ this.initialData = options.initialData;
25974
+ this.placeholder = options.placeholder || '{"customerName": "John Doe", "orderNumber": "12345"}';
25975
+ this.rows = options.rows ?? 10;
25976
+ this.onApply = options.onApply;
25977
+ }
25978
+ createContent() {
25979
+ const container = document.createElement('div');
25980
+ // Textarea for JSON
25981
+ const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
25982
+ container.appendChild(textareaGroup);
25983
+ // Error hint (hidden by default)
25984
+ this.errorHint = this.createHint('');
25985
+ this.errorHint.style.display = 'none';
25986
+ this.errorHint.style.color = '#dc3545';
25987
+ container.appendChild(this.errorHint);
25988
+ // Apply button
25989
+ const applyBtn = this.createButton('Apply Merge Data', { variant: 'primary' });
25990
+ this.addButtonListener(applyBtn, () => this.applyMergeData());
25991
+ container.appendChild(applyBtn);
25992
+ return container;
25993
+ }
25994
+ createTextarea() {
25995
+ this.textarea = document.createElement('textarea');
25996
+ this.textarea.className = 'pc-pane-textarea pc-pane-merge-data-input';
25997
+ this.textarea.rows = this.rows;
25998
+ this.textarea.placeholder = this.placeholder;
25999
+ this.textarea.spellcheck = false;
26000
+ if (this.initialData) {
26001
+ this.textarea.value = JSON.stringify(this.initialData, null, 2);
26002
+ }
26003
+ // Clear error on input
26004
+ this.textarea.addEventListener('input', () => {
26005
+ if (this.errorHint) {
26006
+ this.errorHint.style.display = 'none';
26007
+ }
26008
+ });
26009
+ return this.textarea;
26010
+ }
26011
+ applyMergeData() {
26012
+ if (!this.editor || !this.textarea)
26013
+ return;
26014
+ try {
26015
+ const mergeData = JSON.parse(this.textarea.value);
26016
+ this.editor.applyMergeData(mergeData);
26017
+ if (this.errorHint) {
26018
+ this.errorHint.style.display = 'none';
26019
+ }
26020
+ this.onApply?.(true);
26021
+ }
26022
+ catch (error) {
26023
+ const err = error instanceof Error ? error : new Error(String(error));
26024
+ if (this.errorHint) {
26025
+ if (error instanceof SyntaxError) {
26026
+ this.errorHint.textContent = 'Invalid JSON syntax';
26027
+ }
26028
+ else {
26029
+ this.errorHint.textContent = err.message;
26030
+ }
26031
+ this.errorHint.style.display = 'block';
26032
+ }
26033
+ this.onApply?.(false, err);
26034
+ }
26035
+ }
26036
+ /**
26037
+ * Get the current JSON data from the textarea.
26038
+ * Returns null if the JSON is invalid.
26039
+ */
26040
+ getData() {
26041
+ if (!this.textarea)
26042
+ return null;
26043
+ try {
26044
+ return JSON.parse(this.textarea.value);
26045
+ }
26046
+ catch {
26047
+ return null;
26048
+ }
26049
+ }
26050
+ /**
26051
+ * Set the JSON data in the textarea.
26052
+ */
26053
+ setData(data) {
26054
+ if (this.textarea) {
26055
+ this.textarea.value = JSON.stringify(data, null, 2);
26056
+ if (this.errorHint) {
26057
+ this.errorHint.style.display = 'none';
26058
+ }
26059
+ }
26060
+ }
26061
+ /**
26062
+ * Update the pane (no-op for MergeDataPane as it doesn't track editor state).
26063
+ */
26064
+ update() {
26065
+ // MergeDataPane doesn't need to update from editor state
26066
+ }
26067
+ }
26068
+
26069
+ /**
26070
+ * FormattingPane - Text formatting controls.
26071
+ *
26072
+ * Controls:
26073
+ * - Bold/Italic toggles
26074
+ * - Alignment buttons (left, center, right, justify)
26075
+ * - List buttons (bullet, numbered, indent, outdent)
26076
+ * - Font family/size dropdowns
26077
+ * - Text color and highlight color pickers
26078
+ *
26079
+ * Uses the PCEditor public API:
26080
+ * - editor.getUnifiedFormattingAtCursor()
26081
+ * - editor.applyFormattingWithFallback()
26082
+ * - editor.setPendingFormatting()
26083
+ * - editor.getSavedOrCurrentSelection()
26084
+ * - editor.getUnifiedAlignmentAtCursor()
26085
+ * - editor.setUnifiedAlignment()
26086
+ * - editor.toggleBulletList()
26087
+ * - editor.toggleNumberedList()
26088
+ * - editor.indentParagraph()
26089
+ * - editor.outdentParagraph()
26090
+ * - editor.getListFormatting()
26091
+ */
26092
+ const DEFAULT_FONT_FAMILIES = [
26093
+ 'Arial',
26094
+ 'Times New Roman',
26095
+ 'Georgia',
26096
+ 'Verdana',
26097
+ 'Courier New'
26098
+ ];
26099
+ const DEFAULT_FONT_SIZES = [10, 12, 14, 16, 18, 20, 24, 28, 32, 36];
26100
+ class FormattingPane extends BasePane {
26101
+ constructor(id = 'formatting', options = {}) {
26102
+ super(id, { className: 'pc-pane-formatting', ...options });
26103
+ // Style toggles
26104
+ this.boldBtn = null;
26105
+ this.italicBtn = null;
26106
+ // Alignment buttons
26107
+ this.alignLeftBtn = null;
26108
+ this.alignCenterBtn = null;
26109
+ this.alignRightBtn = null;
26110
+ this.alignJustifyBtn = null;
26111
+ // List buttons
26112
+ this.bulletListBtn = null;
26113
+ this.numberedListBtn = null;
26114
+ this.indentBtn = null;
26115
+ this.outdentBtn = null;
26116
+ // Font controls
26117
+ this.fontFamilySelect = null;
26118
+ this.fontSizeSelect = null;
26119
+ this.colorInput = null;
26120
+ this.highlightInput = null;
26121
+ this.fontFamilies = options.fontFamilies ?? DEFAULT_FONT_FAMILIES;
26122
+ this.fontSizes = options.fontSizes ?? DEFAULT_FONT_SIZES;
26123
+ }
26124
+ attach(options) {
26125
+ super.attach(options);
26126
+ if (this.editor) {
26127
+ // Update on cursor/selection changes
26128
+ const updateHandler = () => this.updateFromEditor();
26129
+ this.editor.on('cursor-changed', updateHandler);
26130
+ this.editor.on('selection-changed', updateHandler);
26131
+ this.editor.on('text-changed', updateHandler);
26132
+ this.editor.on('formatting-changed', updateHandler);
26133
+ this.eventCleanup.push(() => {
26134
+ this.editor?.off('cursor-changed', updateHandler);
26135
+ this.editor?.off('selection-changed', updateHandler);
26136
+ this.editor?.off('text-changed', updateHandler);
26137
+ this.editor?.off('formatting-changed', updateHandler);
26138
+ });
26139
+ // Initial update
26140
+ this.updateFromEditor();
26141
+ }
26142
+ }
26143
+ createContent() {
26144
+ const container = document.createElement('div');
26145
+ // Style section (Bold, Italic)
26146
+ const styleSection = this.createSection('Style');
26147
+ const styleGroup = this.createButtonGroup();
26148
+ this.boldBtn = this.createButton('B');
26149
+ this.boldBtn.title = 'Bold';
26150
+ this.boldBtn.style.fontWeight = 'bold';
26151
+ this.addButtonListener(this.boldBtn, () => this.toggleBold());
26152
+ this.italicBtn = this.createButton('I');
26153
+ this.italicBtn.title = 'Italic';
26154
+ this.italicBtn.style.fontStyle = 'italic';
26155
+ this.addButtonListener(this.italicBtn, () => this.toggleItalic());
26156
+ styleGroup.appendChild(this.boldBtn);
26157
+ styleGroup.appendChild(this.italicBtn);
26158
+ styleSection.appendChild(styleGroup);
26159
+ container.appendChild(styleSection);
26160
+ // Alignment section
26161
+ const alignSection = this.createSection('Alignment');
26162
+ const alignGroup = this.createButtonGroup();
26163
+ this.alignLeftBtn = this.createButton('');
26164
+ this.alignLeftBtn.title = 'Align Left';
26165
+ this.alignLeftBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-left');
26166
+ this.addButtonListener(this.alignLeftBtn, () => this.setAlignment('left'));
26167
+ this.alignCenterBtn = this.createButton('');
26168
+ this.alignCenterBtn.title = 'Center';
26169
+ this.alignCenterBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-center');
26170
+ this.addButtonListener(this.alignCenterBtn, () => this.setAlignment('center'));
26171
+ this.alignRightBtn = this.createButton('');
26172
+ this.alignRightBtn.title = 'Align Right';
26173
+ this.alignRightBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-right');
26174
+ this.addButtonListener(this.alignRightBtn, () => this.setAlignment('right'));
26175
+ this.alignJustifyBtn = this.createButton('');
26176
+ this.alignJustifyBtn.title = 'Justify';
26177
+ this.alignJustifyBtn.classList.add('pc-pane-button--icon', 'pc-pane-button--align-justify');
26178
+ this.addButtonListener(this.alignJustifyBtn, () => this.setAlignment('justify'));
26179
+ alignGroup.appendChild(this.alignLeftBtn);
26180
+ alignGroup.appendChild(this.alignCenterBtn);
26181
+ alignGroup.appendChild(this.alignRightBtn);
26182
+ alignGroup.appendChild(this.alignJustifyBtn);
26183
+ alignSection.appendChild(alignGroup);
26184
+ container.appendChild(alignSection);
26185
+ // Lists section
26186
+ const listsSection = this.createSection('Lists');
26187
+ const listsGroup = this.createButtonGroup();
26188
+ this.bulletListBtn = this.createButton('\u2022'); // •
26189
+ this.bulletListBtn.title = 'Bullet List';
26190
+ this.addButtonListener(this.bulletListBtn, () => this.toggleBulletList());
26191
+ this.numberedListBtn = this.createButton('1.');
26192
+ this.numberedListBtn.title = 'Numbered List';
26193
+ this.addButtonListener(this.numberedListBtn, () => this.toggleNumberedList());
26194
+ this.indentBtn = this.createButton('\u2192'); // →
26195
+ this.indentBtn.title = 'Increase Indent';
26196
+ this.addButtonListener(this.indentBtn, () => this.indent());
26197
+ this.outdentBtn = this.createButton('\u2190'); // ←
26198
+ this.outdentBtn.title = 'Decrease Indent';
26199
+ this.addButtonListener(this.outdentBtn, () => this.outdent());
26200
+ listsGroup.appendChild(this.bulletListBtn);
26201
+ listsGroup.appendChild(this.numberedListBtn);
26202
+ listsGroup.appendChild(this.indentBtn);
26203
+ listsGroup.appendChild(this.outdentBtn);
26204
+ listsSection.appendChild(listsGroup);
26205
+ container.appendChild(listsSection);
26206
+ // Font section
26207
+ const fontSection = this.createSection('Font');
26208
+ this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26209
+ this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26210
+ fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
26211
+ this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26212
+ this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26213
+ fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
26214
+ container.appendChild(fontSection);
26215
+ // Color section
26216
+ const colorSection = this.createSection('Color');
26217
+ const colorRow = this.createRow();
26218
+ const colorGroup = document.createElement('div');
26219
+ this.colorInput = this.createColorInput('#000000');
26220
+ this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26221
+ colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26222
+ colorRow.appendChild(colorGroup);
26223
+ const highlightGroup = document.createElement('div');
26224
+ this.highlightInput = this.createColorInput('#ffff00');
26225
+ this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26226
+ const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
26227
+ const clearHighlightBtn = this.createButton('Clear');
26228
+ clearHighlightBtn.className = 'pc-pane-button';
26229
+ clearHighlightBtn.style.marginLeft = '4px';
26230
+ this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26231
+ highlightForm.appendChild(clearHighlightBtn);
26232
+ highlightGroup.appendChild(highlightForm);
26233
+ colorRow.appendChild(highlightGroup);
26234
+ colorSection.appendChild(colorRow);
26235
+ container.appendChild(colorSection);
26236
+ return container;
26237
+ }
26238
+ updateFromEditor() {
26239
+ if (!this.editor)
26240
+ return;
26241
+ // Get formatting at cursor
26242
+ const formatting = this.editor.getUnifiedFormattingAtCursor();
26243
+ if (formatting) {
26244
+ // Update bold button
26245
+ this.boldBtn?.classList.toggle('pc-pane-button--active', formatting.fontWeight === 'bold');
26246
+ // Update italic button
26247
+ this.italicBtn?.classList.toggle('pc-pane-button--active', formatting.fontStyle === 'italic');
26248
+ // Update font family
26249
+ if (this.fontFamilySelect && formatting.fontFamily) {
26250
+ this.fontFamilySelect.value = formatting.fontFamily;
26251
+ }
26252
+ // Update font size
26253
+ if (this.fontSizeSelect && formatting.fontSize) {
26254
+ this.fontSizeSelect.value = formatting.fontSize.toString();
26255
+ }
26256
+ // Update color
26257
+ if (this.colorInput && formatting.color) {
26258
+ this.colorInput.value = formatting.color;
26259
+ }
26260
+ // Update highlight
26261
+ if (this.highlightInput && formatting.backgroundColor) {
26262
+ this.highlightInput.value = formatting.backgroundColor;
26263
+ }
26264
+ }
26265
+ // Update alignment buttons
26266
+ const alignment = this.editor.getUnifiedAlignmentAtCursor();
26267
+ this.updateAlignmentButtons(alignment);
26268
+ // Update list buttons
26269
+ this.updateListButtons();
26270
+ }
26271
+ updateAlignmentButtons(alignment) {
26272
+ const buttons = [
26273
+ { btn: this.alignLeftBtn, align: 'left' },
26274
+ { btn: this.alignCenterBtn, align: 'center' },
26275
+ { btn: this.alignRightBtn, align: 'right' },
26276
+ { btn: this.alignJustifyBtn, align: 'justify' }
26277
+ ];
26278
+ for (const { btn, align } of buttons) {
26279
+ btn?.classList.toggle('pc-pane-button--active', align === alignment);
26280
+ }
26281
+ }
26282
+ updateListButtons() {
26283
+ if (!this.editor)
26284
+ return;
26285
+ try {
26286
+ const listFormatting = this.editor.getListFormatting();
26287
+ if (listFormatting) {
26288
+ this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26289
+ this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26290
+ }
26291
+ }
26292
+ catch {
26293
+ // No text editing active
26294
+ }
26295
+ }
26296
+ getSelection() {
26297
+ if (!this.editor)
26298
+ return null;
26299
+ return this.editor.getSavedOrCurrentSelection();
26300
+ }
26301
+ applyFormatting(formatting) {
26302
+ if (!this.editor)
26303
+ return;
26304
+ const selection = this.getSelection();
26305
+ try {
26306
+ if (selection) {
26307
+ this.editor.applyFormattingWithFallback(selection.start, selection.end, formatting);
26308
+ }
26309
+ else {
26310
+ this.editor.setPendingFormatting(formatting);
26311
+ }
26312
+ this.editor.clearSavedEditingContext();
26313
+ this.updateFromEditor();
26314
+ this.editor.enableTextInput();
26315
+ }
26316
+ catch (error) {
26317
+ console.error('Formatting error:', error);
26318
+ }
26319
+ }
26320
+ toggleBold() {
26321
+ const isActive = this.boldBtn?.classList.contains('pc-pane-button--active');
26322
+ this.applyFormatting({ fontWeight: isActive ? 'normal' : 'bold' });
26323
+ }
26324
+ toggleItalic() {
26325
+ const isActive = this.italicBtn?.classList.contains('pc-pane-button--active');
26326
+ this.applyFormatting({ fontStyle: isActive ? 'normal' : 'italic' });
26327
+ }
26328
+ applyFontFamily() {
26329
+ if (this.fontFamilySelect) {
26330
+ this.applyFormatting({ fontFamily: this.fontFamilySelect.value });
26331
+ }
26332
+ }
26333
+ applyFontSize() {
26334
+ if (this.fontSizeSelect) {
26335
+ this.applyFormatting({ fontSize: parseInt(this.fontSizeSelect.value, 10) });
26336
+ }
26337
+ }
26338
+ applyTextColor() {
26339
+ if (this.colorInput) {
26340
+ this.applyFormatting({ color: this.colorInput.value });
26341
+ }
26342
+ }
26343
+ applyHighlight() {
26344
+ if (this.highlightInput) {
26345
+ this.applyFormatting({ backgroundColor: this.highlightInput.value });
26346
+ }
26347
+ }
26348
+ clearHighlight() {
26349
+ this.applyFormatting({ backgroundColor: undefined });
26350
+ }
26351
+ setAlignment(alignment) {
26352
+ if (!this.editor)
26353
+ return;
26354
+ try {
26355
+ this.editor.setUnifiedAlignment(alignment);
26356
+ this.updateAlignmentButtons(alignment);
26357
+ }
26358
+ catch (error) {
26359
+ console.error('Alignment error:', error);
26360
+ }
26361
+ }
26362
+ toggleBulletList() {
26363
+ if (!this.editor)
26364
+ return;
26365
+ try {
26366
+ this.editor.toggleBulletList();
26367
+ this.updateListButtons();
26368
+ }
26369
+ catch (error) {
26370
+ console.error('Bullet list error:', error);
26371
+ }
26372
+ }
26373
+ toggleNumberedList() {
26374
+ if (!this.editor)
26375
+ return;
26376
+ try {
26377
+ this.editor.toggleNumberedList();
26378
+ this.updateListButtons();
26379
+ }
26380
+ catch (error) {
26381
+ console.error('Numbered list error:', error);
26382
+ }
26383
+ }
26384
+ indent() {
26385
+ if (!this.editor)
26386
+ return;
26387
+ try {
26388
+ this.editor.indentParagraph();
26389
+ this.updateListButtons();
26390
+ }
26391
+ catch (error) {
26392
+ console.error('Indent error:', error);
26393
+ }
26394
+ }
26395
+ outdent() {
26396
+ if (!this.editor)
26397
+ return;
26398
+ try {
26399
+ this.editor.outdentParagraph();
26400
+ this.updateListButtons();
26401
+ }
26402
+ catch (error) {
26403
+ console.error('Outdent error:', error);
26404
+ }
26405
+ }
26406
+ /**
26407
+ * Update the pane from current editor state.
26408
+ */
26409
+ update() {
26410
+ this.updateFromEditor();
26411
+ }
26412
+ }
26413
+
26414
+ /**
26415
+ * HyperlinkPane - Edit hyperlink URL and title.
26416
+ *
26417
+ * This pane is shown when a hyperlink is selected/cursor is in a hyperlink.
26418
+ *
26419
+ * Uses the PCEditor public API:
26420
+ * - editor.getHyperlinkAt()
26421
+ * - editor.updateHyperlink()
26422
+ * - editor.removeHyperlink()
26423
+ * - editor.getCursorPosition()
26424
+ */
26425
+ class HyperlinkPane extends BasePane {
26426
+ constructor(id = 'hyperlink', options = {}) {
26427
+ super(id, { className: 'pc-pane-hyperlink', ...options });
26428
+ this.urlInput = null;
26429
+ this.titleInput = null;
26430
+ this.rangeHint = null;
26431
+ this.currentHyperlink = null;
26432
+ this.onApply = options.onApply;
26433
+ this.onRemove = options.onRemove;
26434
+ }
26435
+ attach(options) {
26436
+ super.attach(options);
26437
+ if (this.editor) {
26438
+ // Update on cursor changes
26439
+ const updateHandler = () => this.updateFromCursor();
26440
+ this.editor.on('cursor-changed', updateHandler);
26441
+ this.editor.on('selection-changed', updateHandler);
26442
+ this.eventCleanup.push(() => {
26443
+ this.editor?.off('cursor-changed', updateHandler);
26444
+ this.editor?.off('selection-changed', updateHandler);
26445
+ });
26446
+ // Initial update
26447
+ this.updateFromCursor();
26448
+ }
26449
+ }
26450
+ createContent() {
26451
+ const container = document.createElement('div');
26452
+ // URL input
26453
+ this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26454
+ container.appendChild(this.createFormGroup('URL', this.urlInput));
26455
+ // Title input
26456
+ this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26457
+ container.appendChild(this.createFormGroup('Title', this.titleInput));
26458
+ // Apply button
26459
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26460
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26461
+ container.appendChild(applyBtn);
26462
+ // Remove button
26463
+ const removeBtn = this.createButton('Remove Link', { variant: 'danger' });
26464
+ removeBtn.style.marginTop = '0.5rem';
26465
+ this.addButtonListener(removeBtn, () => this.removeHyperlink());
26466
+ container.appendChild(removeBtn);
26467
+ // Range hint
26468
+ this.rangeHint = this.createHint('');
26469
+ container.appendChild(this.rangeHint);
26470
+ return container;
26471
+ }
26472
+ updateFromCursor() {
26473
+ if (!this.editor)
26474
+ return;
26475
+ const cursorPos = this.editor.getCursorPosition();
26476
+ const hyperlink = this.editor.getHyperlinkAt(cursorPos);
26477
+ if (hyperlink) {
26478
+ this.showHyperlink(hyperlink);
26479
+ }
26480
+ else {
26481
+ this.hideHyperlink();
26482
+ }
26483
+ }
26484
+ showHyperlink(hyperlink) {
26485
+ this.currentHyperlink = hyperlink;
26486
+ if (this.urlInput) {
26487
+ this.urlInput.value = hyperlink.url;
26488
+ }
26489
+ if (this.titleInput) {
26490
+ this.titleInput.value = hyperlink.title || '';
26491
+ }
26492
+ if (this.rangeHint) {
26493
+ this.rangeHint.textContent = `Link spans characters ${hyperlink.startIndex} to ${hyperlink.endIndex}`;
26494
+ }
26495
+ // Show the pane
26496
+ this.show();
26497
+ }
26498
+ hideHyperlink() {
26499
+ this.currentHyperlink = null;
26500
+ this.hide();
26501
+ }
26502
+ applyChanges() {
26503
+ if (!this.editor || !this.currentHyperlink)
26504
+ return;
26505
+ try {
26506
+ const url = this.urlInput?.value.trim() || '';
26507
+ const title = this.titleInput?.value.trim() || undefined;
26508
+ if (!url) {
26509
+ this.onApply?.(false, new Error('URL is required'));
26510
+ return;
26511
+ }
26512
+ this.editor.updateHyperlink(this.currentHyperlink.id, { url, title });
26513
+ // Update local reference
26514
+ this.currentHyperlink.url = url;
26515
+ this.currentHyperlink.title = title;
26516
+ this.onApply?.(true);
26517
+ }
26518
+ catch (error) {
26519
+ this.onApply?.(false, error instanceof Error ? error : new Error(String(error)));
26520
+ }
26521
+ }
26522
+ removeHyperlink() {
26523
+ if (!this.editor || !this.currentHyperlink)
26524
+ return;
26525
+ try {
26526
+ this.editor.removeHyperlink(this.currentHyperlink.id);
26527
+ this.hideHyperlink();
26528
+ this.onRemove?.(true);
26529
+ }
26530
+ catch {
26531
+ this.onRemove?.(false);
26532
+ }
26533
+ }
26534
+ /**
26535
+ * Get the currently selected hyperlink.
26536
+ */
26537
+ getCurrentHyperlink() {
26538
+ return this.currentHyperlink;
26539
+ }
26540
+ /**
26541
+ * Check if a hyperlink is currently selected.
26542
+ */
26543
+ hasHyperlink() {
26544
+ return this.currentHyperlink !== null;
26545
+ }
26546
+ /**
26547
+ * Update the pane from current editor state.
26548
+ */
26549
+ update() {
26550
+ this.updateFromCursor();
26551
+ }
26552
+ }
26553
+
26554
+ /**
26555
+ * SubstitutionFieldPane - Edit substitution field properties.
26556
+ *
26557
+ * Shows:
26558
+ * - Field name
26559
+ * - Default value
26560
+ * - Format configuration (value type, number/currency/date formats)
26561
+ *
26562
+ * Uses the PCEditor public API:
26563
+ * - editor.getFieldAt()
26564
+ * - editor.updateField()
26565
+ */
26566
+ class SubstitutionFieldPane extends BasePane {
26567
+ constructor(id = 'substitution-field', options = {}) {
26568
+ super(id, { className: 'pc-pane-substitution-field', ...options });
26569
+ this.fieldNameInput = null;
26570
+ this.fieldDefaultInput = null;
26571
+ this.valueTypeSelect = null;
26572
+ this.numberFormatSelect = null;
26573
+ this.currencyFormatSelect = null;
26574
+ this.dateFormatSelect = null;
26575
+ this.positionHint = null;
26576
+ this.numberFormatGroup = null;
26577
+ this.currencyFormatGroup = null;
26578
+ this.dateFormatGroup = null;
26579
+ this.currentField = null;
26580
+ this.onApplyCallback = options.onApply;
26581
+ }
26582
+ attach(options) {
26583
+ super.attach(options);
26584
+ if (this.editor) {
26585
+ // Listen for field selection events
26586
+ const selectionHandler = (event) => {
26587
+ if (event.type === 'field' && event.field) {
26588
+ this.showField(event.field);
26589
+ }
26590
+ else if (!event.type || event.type !== 'field') ;
26591
+ };
26592
+ this.editor.on('selection-change', selectionHandler);
26593
+ this.eventCleanup.push(() => {
26594
+ this.editor?.off('selection-change', selectionHandler);
26595
+ });
26596
+ }
26597
+ }
26598
+ createContent() {
26599
+ const container = document.createElement('div');
26600
+ // Field name input
26601
+ this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26602
+ container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
26603
+ // Default value input
26604
+ this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26605
+ container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
26606
+ // Value type select
26607
+ this.valueTypeSelect = this.createSelect([
26608
+ { value: '', label: '(None)' },
26609
+ { value: 'number', label: 'Number' },
26610
+ { value: 'currency', label: 'Currency' },
26611
+ { value: 'date', label: 'Date' }
26612
+ ]);
26613
+ this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26614
+ container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
26615
+ // Number format group
26616
+ this.numberFormatGroup = this.createSection();
26617
+ this.numberFormatGroup.style.display = 'none';
26618
+ this.numberFormatSelect = this.createSelect([
26619
+ { value: '0', label: 'Integer (0)' },
26620
+ { value: '0.00', label: 'Two decimals (0.00)' },
26621
+ { value: '0,0', label: 'Thousands separator (0,0)' },
26622
+ { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26623
+ ]);
26624
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
26625
+ container.appendChild(this.numberFormatGroup);
26626
+ // Currency format group
26627
+ this.currencyFormatGroup = this.createSection();
26628
+ this.currencyFormatGroup.style.display = 'none';
26629
+ this.currencyFormatSelect = this.createSelect([
26630
+ { value: 'USD', label: 'USD ($)' },
26631
+ { value: 'EUR', label: 'EUR' },
26632
+ { value: 'GBP', label: 'GBP' },
26633
+ { value: 'JPY', label: 'JPY' }
26634
+ ]);
26635
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
26636
+ container.appendChild(this.currencyFormatGroup);
26637
+ // Date format group
26638
+ this.dateFormatGroup = this.createSection();
26639
+ this.dateFormatGroup.style.display = 'none';
26640
+ this.dateFormatSelect = this.createSelect([
26641
+ { value: 'MMMM D, YYYY', label: 'January 1, 2026' },
26642
+ { value: 'MM/DD/YYYY', label: '01/01/2026' },
26643
+ { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26644
+ { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26645
+ ]);
26646
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
26647
+ container.appendChild(this.dateFormatGroup);
26648
+ // Apply button
26649
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26650
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26651
+ container.appendChild(applyBtn);
26652
+ // Position hint
26653
+ this.positionHint = this.createHint('');
26654
+ container.appendChild(this.positionHint);
26655
+ return container;
26656
+ }
26657
+ updateFormatGroups() {
26658
+ const valueType = this.valueTypeSelect?.value || '';
26659
+ if (this.numberFormatGroup) {
26660
+ this.numberFormatGroup.style.display = valueType === 'number' ? 'block' : 'none';
26661
+ }
26662
+ if (this.currencyFormatGroup) {
26663
+ this.currencyFormatGroup.style.display = valueType === 'currency' ? 'block' : 'none';
26664
+ }
26665
+ if (this.dateFormatGroup) {
26666
+ this.dateFormatGroup.style.display = valueType === 'date' ? 'block' : 'none';
26667
+ }
26668
+ }
26669
+ /**
26670
+ * Show the pane with the given field.
26671
+ */
26672
+ showField(field) {
26673
+ this.currentField = field;
26674
+ if (this.fieldNameInput) {
26675
+ this.fieldNameInput.value = field.fieldName;
26676
+ }
26677
+ if (this.fieldDefaultInput) {
26678
+ this.fieldDefaultInput.value = field.defaultValue || '';
26679
+ }
26680
+ if (this.positionHint) {
26681
+ this.positionHint.textContent = `Field at position ${field.textIndex}`;
26682
+ }
26683
+ // Populate format options
26684
+ if (this.valueTypeSelect) {
26685
+ this.valueTypeSelect.value = field.formatConfig?.valueType || '';
26686
+ }
26687
+ if (this.numberFormatSelect && field.formatConfig?.numberFormat) {
26688
+ this.numberFormatSelect.value = field.formatConfig.numberFormat;
26689
+ }
26690
+ if (this.currencyFormatSelect && field.formatConfig?.currencyFormat) {
26691
+ this.currencyFormatSelect.value = field.formatConfig.currencyFormat;
26692
+ }
26693
+ if (this.dateFormatSelect && field.formatConfig?.dateFormat) {
26694
+ this.dateFormatSelect.value = field.formatConfig.dateFormat;
26695
+ }
26696
+ this.updateFormatGroups();
26697
+ this.show();
26698
+ }
26699
+ /**
26700
+ * Hide the pane and clear the current field.
26701
+ */
26702
+ hideField() {
26703
+ this.currentField = null;
26704
+ this.hide();
26705
+ }
26706
+ applyChanges() {
26707
+ if (!this.editor || !this.currentField) {
26708
+ this.onApplyCallback?.(false, new Error('No field selected'));
26709
+ return;
26710
+ }
26711
+ const fieldName = this.fieldNameInput?.value.trim();
26712
+ if (!fieldName) {
26713
+ this.onApplyCallback?.(false, new Error('Field name cannot be empty'));
26714
+ return;
26715
+ }
26716
+ const updates = {};
26717
+ if (fieldName !== this.currentField.fieldName) {
26718
+ updates.fieldName = fieldName;
26719
+ }
26720
+ const defaultValue = this.fieldDefaultInput?.value || undefined;
26721
+ if (defaultValue !== this.currentField.defaultValue) {
26722
+ updates.defaultValue = defaultValue;
26723
+ }
26724
+ // Build format config
26725
+ const valueType = this.valueTypeSelect?.value;
26726
+ if (valueType) {
26727
+ const formatConfig = {
26728
+ valueType: valueType
26729
+ };
26730
+ if (valueType === 'number' && this.numberFormatSelect?.value) {
26731
+ formatConfig.numberFormat = this.numberFormatSelect.value;
26732
+ }
26733
+ else if (valueType === 'currency' && this.currencyFormatSelect?.value) {
26734
+ formatConfig.currencyFormat = this.currencyFormatSelect.value;
26735
+ }
26736
+ else if (valueType === 'date' && this.dateFormatSelect?.value) {
26737
+ formatConfig.dateFormat = this.dateFormatSelect.value;
26738
+ }
26739
+ updates.formatConfig = formatConfig;
26740
+ }
26741
+ else if (this.currentField.formatConfig) {
26742
+ updates.formatConfig = undefined;
26743
+ }
26744
+ if (Object.keys(updates).length === 0) {
26745
+ return; // No changes
26746
+ }
26747
+ try {
26748
+ const success = this.editor.updateField(this.currentField.textIndex, updates);
26749
+ if (success) {
26750
+ // Update the current field reference
26751
+ this.currentField = this.editor.getFieldAt(this.currentField.textIndex) || null;
26752
+ this.onApplyCallback?.(true);
26753
+ }
26754
+ else {
26755
+ this.onApplyCallback?.(false, new Error('Failed to update field'));
26756
+ }
26757
+ }
26758
+ catch (error) {
26759
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
26760
+ }
26761
+ }
26762
+ /**
26763
+ * Get the currently selected field.
26764
+ */
26765
+ getCurrentField() {
26766
+ return this.currentField;
26767
+ }
26768
+ /**
26769
+ * Check if a field is currently selected.
26770
+ */
26771
+ hasField() {
26772
+ return this.currentField !== null;
26773
+ }
26774
+ /**
26775
+ * Update the pane from current editor state.
26776
+ */
26777
+ update() {
26778
+ // Field pane doesn't auto-update - it's driven by selection events
26779
+ }
26780
+ }
26781
+
26782
+ /**
26783
+ * RepeatingSectionPane - Edit repeating section (loop) properties.
26784
+ *
26785
+ * Shows:
26786
+ * - Field path (array property in merge data)
26787
+ * - Position information
26788
+ *
26789
+ * Uses the PCEditor public API:
26790
+ * - editor.getRepeatingSection()
26791
+ * - editor.updateRepeatingSectionFieldPath()
26792
+ * - editor.removeRepeatingSection()
26793
+ */
26794
+ class RepeatingSectionPane extends BasePane {
26795
+ constructor(id = 'repeating-section', options = {}) {
26796
+ super(id, { className: 'pc-pane-repeating-section', ...options });
26797
+ this.fieldPathInput = null;
26798
+ this.positionHint = null;
26799
+ this.currentSection = null;
26800
+ this.onApplyCallback = options.onApply;
26801
+ this.onRemoveCallback = options.onRemove;
26802
+ }
26803
+ attach(options) {
26804
+ super.attach(options);
26805
+ if (this.editor) {
26806
+ // Listen for repeating section selection
26807
+ const selectionHandler = (event) => {
26808
+ if (event.type === 'repeating-section' && event.sectionId) {
26809
+ const section = this.editor?.getRepeatingSection(event.sectionId);
26810
+ if (section) {
26811
+ this.showSection(section);
26812
+ }
26813
+ }
26814
+ };
26815
+ const removedHandler = () => {
26816
+ this.hideSection();
26817
+ };
26818
+ this.editor.on('selection-change', selectionHandler);
26819
+ this.editor.on('repeating-section-removed', removedHandler);
26820
+ this.eventCleanup.push(() => {
26821
+ this.editor?.off('selection-change', selectionHandler);
26822
+ this.editor?.off('repeating-section-removed', removedHandler);
26823
+ });
26824
+ }
26825
+ }
26826
+ createContent() {
26827
+ const container = document.createElement('div');
26828
+ // Field path input
26829
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
26830
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
26831
+ hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
26832
+ }));
26833
+ // Apply button
26834
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26835
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26836
+ container.appendChild(applyBtn);
26837
+ // Remove button
26838
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
26839
+ removeBtn.style.marginTop = '0.5rem';
26840
+ this.addButtonListener(removeBtn, () => this.removeSection());
26841
+ container.appendChild(removeBtn);
26842
+ // Position hint
26843
+ this.positionHint = this.createHint('');
26844
+ container.appendChild(this.positionHint);
26845
+ return container;
26846
+ }
26847
+ /**
26848
+ * Show the pane with the given section.
26849
+ */
26850
+ showSection(section) {
26851
+ this.currentSection = section;
26852
+ if (this.fieldPathInput) {
26853
+ this.fieldPathInput.value = section.fieldPath;
26854
+ }
26855
+ if (this.positionHint) {
26856
+ this.positionHint.textContent = `Loop from position ${section.startIndex} to ${section.endIndex}`;
26857
+ }
26858
+ this.show();
26859
+ }
26860
+ /**
26861
+ * Hide the pane and clear the current section.
26862
+ */
26863
+ hideSection() {
26864
+ this.currentSection = null;
26865
+ this.hide();
26866
+ }
26867
+ applyChanges() {
26868
+ if (!this.editor || !this.currentSection) {
26869
+ this.onApplyCallback?.(false, new Error('No section selected'));
26870
+ return;
26871
+ }
26872
+ const fieldPath = this.fieldPathInput?.value.trim();
26873
+ if (!fieldPath) {
26874
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
26875
+ return;
26876
+ }
26877
+ if (fieldPath === this.currentSection.fieldPath) {
26878
+ return; // No changes
26879
+ }
26880
+ try {
26881
+ const success = this.editor.updateRepeatingSectionFieldPath(this.currentSection.id, fieldPath);
26882
+ if (success) {
26883
+ // Update the current section reference
26884
+ this.currentSection = this.editor.getRepeatingSection(this.currentSection.id) || null;
26885
+ if (this.currentSection) {
26886
+ this.showSection(this.currentSection);
26887
+ }
26888
+ this.onApplyCallback?.(true);
26889
+ }
26890
+ else {
26891
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
26892
+ }
26893
+ }
26894
+ catch (error) {
26895
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
26896
+ }
26897
+ }
26898
+ removeSection() {
26899
+ if (!this.editor || !this.currentSection)
26900
+ return;
26901
+ try {
26902
+ this.editor.removeRepeatingSection(this.currentSection.id);
26903
+ this.hideSection();
26904
+ this.onRemoveCallback?.(true);
26905
+ }
26906
+ catch {
26907
+ this.onRemoveCallback?.(false);
26908
+ }
26909
+ }
26910
+ /**
26911
+ * Get the currently selected section.
26912
+ */
26913
+ getCurrentSection() {
26914
+ return this.currentSection;
26915
+ }
26916
+ /**
26917
+ * Check if a section is currently selected.
26918
+ */
26919
+ hasSection() {
26920
+ return this.currentSection !== null;
26921
+ }
26922
+ /**
26923
+ * Update the pane from current editor state.
26924
+ */
26925
+ update() {
26926
+ // Section pane doesn't auto-update - it's driven by selection events
26927
+ }
26928
+ }
26929
+
26930
+ /**
26931
+ * TableRowLoopPane - Edit table row loop properties.
26932
+ *
26933
+ * Shows:
26934
+ * - Field path (array property in merge data)
26935
+ * - Row range information
26936
+ *
26937
+ * Uses the TableObject API:
26938
+ * - table.getRowLoop()
26939
+ * - table.updateRowLoopFieldPath()
26940
+ * - table.removeRowLoop()
26941
+ */
26942
+ class TableRowLoopPane extends BasePane {
26943
+ constructor(id = 'table-row-loop', options = {}) {
26944
+ super(id, { className: 'pc-pane-table-row-loop', ...options });
26945
+ this.fieldPathInput = null;
26946
+ this.rangeHint = null;
26947
+ this.currentLoop = null;
26948
+ this.currentTable = null;
26949
+ this.onApplyCallback = options.onApply;
26950
+ this.onRemoveCallback = options.onRemove;
26951
+ }
26952
+ attach(options) {
26953
+ super.attach(options);
26954
+ // Table row loop pane is typically shown manually when a table's row loop is selected
26955
+ // The consumer is responsible for calling showLoop() with the table and loop
26956
+ }
26957
+ createContent() {
26958
+ const container = document.createElement('div');
26959
+ // Field path input
26960
+ this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
26961
+ container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
26962
+ hint: 'Path to array in merge data (e.g., "items" or "orders")'
26963
+ }));
26964
+ // Apply button
26965
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26966
+ this.addButtonListener(applyBtn, () => this.applyChanges());
26967
+ container.appendChild(applyBtn);
26968
+ // Remove button
26969
+ const removeBtn = this.createButton('Remove Loop', { variant: 'danger' });
26970
+ removeBtn.style.marginTop = '0.5rem';
26971
+ this.addButtonListener(removeBtn, () => this.removeLoop());
26972
+ container.appendChild(removeBtn);
26973
+ // Range hint
26974
+ this.rangeHint = this.createHint('');
26975
+ container.appendChild(this.rangeHint);
26976
+ return container;
26977
+ }
26978
+ /**
26979
+ * Show the pane with the given table and loop.
26980
+ */
26981
+ showLoop(table, loop) {
26982
+ this.currentTable = table;
26983
+ this.currentLoop = loop;
26984
+ if (this.fieldPathInput) {
26985
+ this.fieldPathInput.value = loop.fieldPath;
26986
+ }
26987
+ if (this.rangeHint) {
26988
+ this.rangeHint.textContent = `Rows ${loop.startRowIndex} - ${loop.endRowIndex}`;
26989
+ }
26990
+ this.show();
26991
+ }
26992
+ /**
26993
+ * Hide the pane and clear current loop.
26994
+ */
26995
+ hideLoop() {
26996
+ this.currentTable = null;
26997
+ this.currentLoop = null;
26998
+ this.hide();
26999
+ }
27000
+ applyChanges() {
27001
+ if (!this.currentTable || !this.currentLoop) {
27002
+ this.onApplyCallback?.(false, new Error('No loop selected'));
27003
+ return;
27004
+ }
27005
+ const fieldPath = this.fieldPathInput?.value.trim();
27006
+ if (!fieldPath) {
27007
+ this.onApplyCallback?.(false, new Error('Field path cannot be empty'));
27008
+ return;
27009
+ }
27010
+ if (fieldPath === this.currentLoop.fieldPath) {
27011
+ return; // No changes
27012
+ }
27013
+ try {
27014
+ const success = this.currentTable.updateRowLoopFieldPath(this.currentLoop.id, fieldPath);
27015
+ if (success) {
27016
+ // Update the current loop reference
27017
+ this.currentLoop = this.currentTable.getRowLoop(this.currentLoop.id) || null;
27018
+ if (this.currentLoop) {
27019
+ this.showLoop(this.currentTable, this.currentLoop);
27020
+ }
27021
+ this.onApplyCallback?.(true);
27022
+ }
27023
+ else {
27024
+ this.onApplyCallback?.(false, new Error('Failed to update loop'));
27025
+ }
27026
+ }
27027
+ catch (error) {
27028
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27029
+ }
27030
+ }
27031
+ removeLoop() {
27032
+ if (!this.currentTable || !this.currentLoop)
27033
+ return;
27034
+ try {
27035
+ const success = this.currentTable.removeRowLoop(this.currentLoop.id);
27036
+ if (success) {
27037
+ this.hideLoop();
27038
+ this.onRemoveCallback?.(true);
27039
+ }
27040
+ else {
27041
+ this.onRemoveCallback?.(false);
27042
+ }
27043
+ }
27044
+ catch {
27045
+ this.onRemoveCallback?.(false);
27046
+ }
27047
+ }
27048
+ /**
27049
+ * Get the currently selected loop.
27050
+ */
27051
+ getCurrentLoop() {
27052
+ return this.currentLoop;
27053
+ }
27054
+ /**
27055
+ * Get the currently selected table.
27056
+ */
27057
+ getCurrentTable() {
27058
+ return this.currentTable;
27059
+ }
27060
+ /**
27061
+ * Check if a loop is currently selected.
27062
+ */
27063
+ hasLoop() {
27064
+ return this.currentLoop !== null;
27065
+ }
27066
+ /**
27067
+ * Update the pane from current editor state.
27068
+ */
27069
+ update() {
27070
+ // Table row loop pane doesn't auto-update - it's driven by showLoop() calls
27071
+ }
27072
+ }
27073
+
27074
+ /**
27075
+ * TextBoxPane - Edit text box properties.
27076
+ *
27077
+ * Shows:
27078
+ * - Position (inline, block, relative)
27079
+ * - Relative offset (for relative positioning)
27080
+ * - Background color
27081
+ * - Border (width, color, style)
27082
+ * - Padding
27083
+ *
27084
+ * Uses the PCEditor public API:
27085
+ * - editor.getSelectedTextBox()
27086
+ * - editor.updateTextBox()
27087
+ */
27088
+ class TextBoxPane extends BasePane {
27089
+ constructor(id = 'textbox', options = {}) {
27090
+ super(id, { className: 'pc-pane-textbox', ...options });
27091
+ this.positionSelect = null;
27092
+ this.offsetGroup = null;
27093
+ this.offsetXInput = null;
27094
+ this.offsetYInput = null;
27095
+ this.bgColorInput = null;
27096
+ this.borderWidthInput = null;
27097
+ this.borderColorInput = null;
27098
+ this.borderStyleSelect = null;
27099
+ this.paddingInput = null;
27100
+ this.currentTextBox = null;
27101
+ this.onApplyCallback = options.onApply;
27102
+ }
27103
+ attach(options) {
27104
+ super.attach(options);
27105
+ if (this.editor) {
27106
+ // Listen for selection changes
27107
+ const updateHandler = () => this.updateFromSelection();
27108
+ this.editor.on('selection-change', updateHandler);
27109
+ this.editor.on('textbox-updated', updateHandler);
27110
+ this.eventCleanup.push(() => {
27111
+ this.editor?.off('selection-change', updateHandler);
27112
+ this.editor?.off('textbox-updated', updateHandler);
27113
+ });
27114
+ // Initial update
27115
+ this.updateFromSelection();
27116
+ }
27117
+ }
27118
+ createContent() {
27119
+ const container = document.createElement('div');
27120
+ // Position section
27121
+ const positionSection = this.createSection('Position');
27122
+ this.positionSelect = this.createSelect([
27123
+ { value: 'inline', label: 'Inline' },
27124
+ { value: 'block', label: 'Block' },
27125
+ { value: 'relative', label: 'Relative' }
27126
+ ], 'inline');
27127
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27128
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27129
+ // Offset group (only visible for relative positioning)
27130
+ this.offsetGroup = document.createElement('div');
27131
+ this.offsetGroup.style.display = 'none';
27132
+ const offsetRow = this.createRow();
27133
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27134
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27135
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27136
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27137
+ this.offsetGroup.appendChild(offsetRow);
27138
+ positionSection.appendChild(this.offsetGroup);
27139
+ container.appendChild(positionSection);
27140
+ // Background section
27141
+ const bgSection = this.createSection('Background');
27142
+ this.bgColorInput = this.createColorInput('#ffffff');
27143
+ bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
27144
+ container.appendChild(bgSection);
27145
+ // Border section
27146
+ const borderSection = this.createSection('Border');
27147
+ const borderRow = this.createRow();
27148
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27149
+ this.borderColorInput = this.createColorInput('#cccccc');
27150
+ borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27151
+ borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27152
+ borderSection.appendChild(borderRow);
27153
+ this.borderStyleSelect = this.createSelect([
27154
+ { value: 'solid', label: 'Solid' },
27155
+ { value: 'dashed', label: 'Dashed' },
27156
+ { value: 'dotted', label: 'Dotted' },
27157
+ { value: 'none', label: 'None' }
27158
+ ], 'solid');
27159
+ borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27160
+ container.appendChild(borderSection);
27161
+ // Padding section
27162
+ const paddingSection = this.createSection('Padding');
27163
+ this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27164
+ paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
27165
+ container.appendChild(paddingSection);
27166
+ // Apply button
27167
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27168
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27169
+ container.appendChild(applyBtn);
27170
+ return container;
27171
+ }
27172
+ updateFromSelection() {
27173
+ if (!this.editor)
27174
+ return;
27175
+ const textBox = this.editor.getSelectedTextBox?.();
27176
+ if (textBox && !textBox.editing) {
27177
+ this.showTextBox(textBox);
27178
+ }
27179
+ else {
27180
+ this.hideTextBox();
27181
+ }
27182
+ }
27183
+ /**
27184
+ * Show the pane with the given text box.
27185
+ */
27186
+ showTextBox(textBox) {
27187
+ this.currentTextBox = textBox;
27188
+ // Populate position
27189
+ if (this.positionSelect) {
27190
+ this.positionSelect.value = textBox.position || 'inline';
27191
+ }
27192
+ this.updateOffsetVisibility();
27193
+ // Populate offset
27194
+ if (this.offsetXInput) {
27195
+ this.offsetXInput.value = String(textBox.relativeOffset?.x ?? 0);
27196
+ }
27197
+ if (this.offsetYInput) {
27198
+ this.offsetYInput.value = String(textBox.relativeOffset?.y ?? 0);
27199
+ }
27200
+ // Populate background
27201
+ if (this.bgColorInput) {
27202
+ this.bgColorInput.value = textBox.backgroundColor || '#ffffff';
27203
+ }
27204
+ // Populate border (use first side with non-none style)
27205
+ const border = textBox.border;
27206
+ const activeBorder = border.top.style !== 'none' ? border.top :
27207
+ border.right.style !== 'none' ? border.right :
27208
+ border.bottom.style !== 'none' ? border.bottom :
27209
+ border.left.style !== 'none' ? border.left : border.top;
27210
+ if (this.borderWidthInput) {
27211
+ this.borderWidthInput.value = String(activeBorder.width);
27212
+ }
27213
+ if (this.borderColorInput) {
27214
+ this.borderColorInput.value = activeBorder.color;
27215
+ }
27216
+ if (this.borderStyleSelect) {
27217
+ this.borderStyleSelect.value = activeBorder.style;
27218
+ }
27219
+ // Populate padding
27220
+ if (this.paddingInput) {
27221
+ this.paddingInput.value = String(textBox.padding ?? 8);
27222
+ }
27223
+ this.show();
27224
+ }
27225
+ /**
27226
+ * Hide the pane and clear current text box.
27227
+ */
27228
+ hideTextBox() {
27229
+ this.currentTextBox = null;
27230
+ this.hide();
27231
+ }
27232
+ updateOffsetVisibility() {
27233
+ if (this.offsetGroup && this.positionSelect) {
27234
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27235
+ }
27236
+ }
27237
+ applyChanges() {
27238
+ if (!this.editor || !this.currentTextBox) {
27239
+ this.onApplyCallback?.(false, new Error('No text box selected'));
27240
+ return;
27241
+ }
27242
+ const updates = {};
27243
+ // Position
27244
+ if (this.positionSelect) {
27245
+ updates.position = this.positionSelect.value;
27246
+ }
27247
+ // Relative offset
27248
+ if (this.positionSelect?.value === 'relative') {
27249
+ updates.relativeOffset = {
27250
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27251
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27252
+ };
27253
+ }
27254
+ // Background color
27255
+ if (this.bgColorInput) {
27256
+ updates.backgroundColor = this.bgColorInput.value;
27257
+ }
27258
+ // Border
27259
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27260
+ const color = this.borderColorInput?.value || '#cccccc';
27261
+ const style = (this.borderStyleSelect?.value || 'solid');
27262
+ const borderSide = { width, color, style };
27263
+ updates.border = {
27264
+ top: { ...borderSide },
27265
+ right: { ...borderSide },
27266
+ bottom: { ...borderSide },
27267
+ left: { ...borderSide }
27268
+ };
27269
+ // Padding
27270
+ if (this.paddingInput) {
27271
+ updates.padding = parseInt(this.paddingInput.value, 10);
27272
+ }
27273
+ try {
27274
+ const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27275
+ if (success) {
27276
+ this.onApplyCallback?.(true);
27277
+ }
27278
+ else {
27279
+ this.onApplyCallback?.(false, new Error('Failed to update text box'));
27280
+ }
27281
+ }
27282
+ catch (error) {
27283
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27284
+ }
27285
+ }
27286
+ /**
27287
+ * Get the currently selected text box.
27288
+ */
27289
+ getCurrentTextBox() {
27290
+ return this.currentTextBox;
27291
+ }
27292
+ /**
27293
+ * Check if a text box is currently selected.
27294
+ */
27295
+ hasTextBox() {
27296
+ return this.currentTextBox !== null;
27297
+ }
27298
+ /**
27299
+ * Update the pane from current editor state.
27300
+ */
27301
+ update() {
27302
+ this.updateFromSelection();
27303
+ }
27304
+ }
27305
+
27306
+ /**
27307
+ * ImagePane - Edit image properties.
27308
+ *
27309
+ * Shows:
27310
+ * - Position (inline, block, relative)
27311
+ * - Relative offset (for relative positioning)
27312
+ * - Fit mode (contain, cover, fill, none, tile)
27313
+ * - Resize mode (free, locked-aspect-ratio)
27314
+ * - Alt text
27315
+ * - Source file picker
27316
+ *
27317
+ * Uses the PCEditor public API:
27318
+ * - editor.getSelectedImage()
27319
+ * - editor.updateImage()
27320
+ * - editor.setImageSource()
27321
+ */
27322
+ class ImagePane extends BasePane {
27323
+ constructor(id = 'image', options = {}) {
27324
+ super(id, { className: 'pc-pane-image', ...options });
27325
+ this.positionSelect = null;
27326
+ this.offsetGroup = null;
27327
+ this.offsetXInput = null;
27328
+ this.offsetYInput = null;
27329
+ this.fitModeSelect = null;
27330
+ this.resizeModeSelect = null;
27331
+ this.altTextInput = null;
27332
+ this.fileInput = null;
27333
+ this.currentImage = null;
27334
+ this.maxImageWidth = options.maxImageWidth ?? 400;
27335
+ this.maxImageHeight = options.maxImageHeight ?? 400;
27336
+ this.onApplyCallback = options.onApply;
27337
+ }
27338
+ attach(options) {
27339
+ super.attach(options);
27340
+ if (this.editor) {
27341
+ // Listen for selection changes
27342
+ const updateHandler = () => this.updateFromSelection();
27343
+ this.editor.on('selection-change', updateHandler);
27344
+ this.editor.on('image-updated', updateHandler);
27345
+ this.eventCleanup.push(() => {
27346
+ this.editor?.off('selection-change', updateHandler);
27347
+ this.editor?.off('image-updated', updateHandler);
27348
+ });
27349
+ // Initial update
27350
+ this.updateFromSelection();
27351
+ }
27352
+ }
27353
+ createContent() {
27354
+ const container = document.createElement('div');
27355
+ // Position section
27356
+ const positionSection = this.createSection('Position');
27357
+ this.positionSelect = this.createSelect([
27358
+ { value: 'inline', label: 'Inline' },
27359
+ { value: 'block', label: 'Block' },
27360
+ { value: 'relative', label: 'Relative' }
27361
+ ], 'inline');
27362
+ this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27363
+ positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
27364
+ // Offset group (only visible for relative positioning)
27365
+ this.offsetGroup = document.createElement('div');
27366
+ this.offsetGroup.style.display = 'none';
27367
+ const offsetRow = this.createRow();
27368
+ this.offsetXInput = this.createNumberInput({ value: 0 });
27369
+ this.offsetYInput = this.createNumberInput({ value: 0 });
27370
+ offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27371
+ offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
27372
+ this.offsetGroup.appendChild(offsetRow);
27373
+ positionSection.appendChild(this.offsetGroup);
27374
+ container.appendChild(positionSection);
27375
+ // Fit mode section
27376
+ const fitSection = this.createSection('Display');
27377
+ this.fitModeSelect = this.createSelect([
27378
+ { value: 'contain', label: 'Contain' },
27379
+ { value: 'cover', label: 'Cover' },
27380
+ { value: 'fill', label: 'Fill' },
27381
+ { value: 'none', label: 'None (original size)' },
27382
+ { value: 'tile', label: 'Tile' }
27383
+ ], 'contain');
27384
+ fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
27385
+ this.resizeModeSelect = this.createSelect([
27386
+ { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27387
+ { value: 'free', label: 'Free Resize' }
27388
+ ], 'locked-aspect-ratio');
27389
+ fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27390
+ container.appendChild(fitSection);
27391
+ // Alt text section
27392
+ const altSection = this.createSection('Accessibility');
27393
+ this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27394
+ altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27395
+ container.appendChild(altSection);
27396
+ // Source section
27397
+ const sourceSection = this.createSection('Source');
27398
+ this.fileInput = document.createElement('input');
27399
+ this.fileInput.type = 'file';
27400
+ this.fileInput.accept = 'image/*';
27401
+ this.fileInput.style.display = 'none';
27402
+ this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27403
+ sourceSection.appendChild(this.fileInput);
27404
+ const changeSourceBtn = this.createButton('Change Image...');
27405
+ this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27406
+ sourceSection.appendChild(changeSourceBtn);
27407
+ container.appendChild(sourceSection);
27408
+ // Apply button
27409
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27410
+ this.addButtonListener(applyBtn, () => this.applyChanges());
27411
+ container.appendChild(applyBtn);
27412
+ return container;
27413
+ }
27414
+ updateFromSelection() {
27415
+ if (!this.editor)
27416
+ return;
27417
+ const image = this.editor.getSelectedImage?.();
27418
+ if (image) {
27419
+ this.showImage(image);
27420
+ }
27421
+ else {
27422
+ this.hideImage();
27423
+ }
27424
+ }
27425
+ /**
27426
+ * Show the pane with the given image.
27427
+ */
27428
+ showImage(image) {
27429
+ this.currentImage = image;
27430
+ // Populate position
27431
+ if (this.positionSelect) {
27432
+ this.positionSelect.value = image.position || 'inline';
27433
+ }
27434
+ this.updateOffsetVisibility();
27435
+ // Populate offset
27436
+ if (this.offsetXInput) {
27437
+ this.offsetXInput.value = String(image.relativeOffset?.x ?? 0);
27438
+ }
27439
+ if (this.offsetYInput) {
27440
+ this.offsetYInput.value = String(image.relativeOffset?.y ?? 0);
27441
+ }
27442
+ // Populate fit mode
27443
+ if (this.fitModeSelect) {
27444
+ this.fitModeSelect.value = image.fit || 'contain';
27445
+ }
27446
+ // Populate resize mode
27447
+ if (this.resizeModeSelect) {
27448
+ this.resizeModeSelect.value = image.resizeMode || 'locked-aspect-ratio';
27449
+ }
27450
+ // Populate alt text
27451
+ if (this.altTextInput) {
27452
+ this.altTextInput.value = image.alt || '';
27453
+ }
27454
+ this.show();
27455
+ }
27456
+ /**
27457
+ * Hide the pane and clear current image.
27458
+ */
27459
+ hideImage() {
27460
+ this.currentImage = null;
27461
+ this.hide();
27462
+ }
27463
+ updateOffsetVisibility() {
27464
+ if (this.offsetGroup && this.positionSelect) {
27465
+ this.offsetGroup.style.display = this.positionSelect.value === 'relative' ? 'block' : 'none';
27466
+ }
27467
+ }
27468
+ handleFileChange(event) {
27469
+ if (!this.editor || !this.currentImage)
27470
+ return;
27471
+ const input = event.target;
27472
+ const file = input.files?.[0];
27473
+ if (!file)
27474
+ return;
27475
+ const reader = new FileReader();
27476
+ reader.onload = (e) => {
27477
+ const dataUrl = e.target?.result;
27478
+ if (dataUrl && this.currentImage && this.editor) {
27479
+ this.editor.setImageSource(this.currentImage.id, dataUrl, {
27480
+ maxWidth: this.maxImageWidth,
27481
+ maxHeight: this.maxImageHeight
27482
+ });
27483
+ }
27484
+ };
27485
+ reader.readAsDataURL(file);
27486
+ // Reset file input so the same file can be selected again
27487
+ input.value = '';
27488
+ }
27489
+ applyChanges() {
27490
+ if (!this.editor || !this.currentImage) {
27491
+ this.onApplyCallback?.(false, new Error('No image selected'));
27492
+ return;
27493
+ }
27494
+ const updates = {};
27495
+ // Position
27496
+ if (this.positionSelect) {
27497
+ updates.position = this.positionSelect.value;
27498
+ }
27499
+ // Relative offset
27500
+ if (this.positionSelect?.value === 'relative') {
27501
+ updates.relativeOffset = {
27502
+ x: parseInt(this.offsetXInput?.value || '0', 10),
27503
+ y: parseInt(this.offsetYInput?.value || '0', 10)
27504
+ };
27505
+ }
27506
+ // Fit mode
27507
+ if (this.fitModeSelect) {
27508
+ updates.fit = this.fitModeSelect.value;
27509
+ }
27510
+ // Resize mode
27511
+ if (this.resizeModeSelect) {
27512
+ updates.resizeMode = this.resizeModeSelect.value;
27513
+ }
27514
+ // Alt text
27515
+ if (this.altTextInput) {
27516
+ updates.alt = this.altTextInput.value;
27517
+ }
27518
+ try {
27519
+ const success = this.editor.updateImage(this.currentImage.id, updates);
27520
+ if (success) {
27521
+ this.onApplyCallback?.(true);
27522
+ }
27523
+ else {
27524
+ this.onApplyCallback?.(false, new Error('Failed to update image'));
27525
+ }
27526
+ }
27527
+ catch (error) {
27528
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
27529
+ }
27530
+ }
27531
+ /**
27532
+ * Get the currently selected image.
27533
+ */
27534
+ getCurrentImage() {
27535
+ return this.currentImage;
27536
+ }
27537
+ /**
27538
+ * Check if an image is currently selected.
27539
+ */
27540
+ hasImage() {
27541
+ return this.currentImage !== null;
27542
+ }
27543
+ /**
27544
+ * Update the pane from current editor state.
27545
+ */
27546
+ update() {
27547
+ this.updateFromSelection();
27548
+ }
27549
+ }
27550
+
27551
+ /**
27552
+ * TablePane - Edit table properties.
27553
+ *
27554
+ * Shows:
27555
+ * - Table structure (row/column count)
27556
+ * - Row/column insertion/removal
27557
+ * - Header rows/columns
27558
+ * - Default cell padding and border color
27559
+ * - Cell-specific formatting (background, borders)
27560
+ *
27561
+ * Uses the PCEditor public API:
27562
+ * - editor.getFocusedTable()
27563
+ * - editor.tableInsertRow()
27564
+ * - editor.tableRemoveRow()
27565
+ * - editor.tableInsertColumn()
27566
+ * - editor.tableRemoveColumn()
27567
+ *
27568
+ * And TableObject methods:
27569
+ * - table.setHeaderRowCount()
27570
+ * - table.setHeaderColumnCount()
27571
+ * - table.getCell()
27572
+ * - table.getCellsInRange()
27573
+ */
27574
+ class TablePane extends BasePane {
27575
+ constructor(id = 'table', options = {}) {
27576
+ super(id, { className: 'pc-pane-table', ...options });
27577
+ // Structure info
27578
+ this.rowCountDisplay = null;
27579
+ this.colCountDisplay = null;
27580
+ this.cellSelectionDisplay = null;
27581
+ // Header controls
27582
+ this.headerRowInput = null;
27583
+ this.headerColInput = null;
27584
+ // Default controls
27585
+ this.defaultPaddingInput = null;
27586
+ this.defaultBorderColorInput = null;
27587
+ // Cell formatting controls
27588
+ this.cellBgColorInput = null;
27589
+ this.borderTopCheck = null;
27590
+ this.borderRightCheck = null;
27591
+ this.borderBottomCheck = null;
27592
+ this.borderLeftCheck = null;
27593
+ this.borderWidthInput = null;
27594
+ this.borderColorInput = null;
27595
+ this.borderStyleSelect = null;
27596
+ this.currentTable = null;
27597
+ this.onApplyCallback = options.onApply;
27598
+ }
27599
+ attach(options) {
27600
+ super.attach(options);
27601
+ if (this.editor) {
27602
+ // Listen for selection/focus changes
27603
+ const updateHandler = () => this.updateFromFocusedTable();
27604
+ this.editor.on('selection-change', updateHandler);
27605
+ this.editor.on('table-cell-focus', updateHandler);
27606
+ this.editor.on('table-cell-selection', updateHandler);
27607
+ this.eventCleanup.push(() => {
27608
+ this.editor?.off('selection-change', updateHandler);
27609
+ this.editor?.off('table-cell-focus', updateHandler);
27610
+ this.editor?.off('table-cell-selection', updateHandler);
27611
+ });
27612
+ // Initial update
27613
+ this.updateFromFocusedTable();
27614
+ }
27615
+ }
27616
+ createContent() {
27617
+ const container = document.createElement('div');
27618
+ // Structure section
27619
+ const structureSection = this.createSection('Structure');
27620
+ const structureInfo = document.createElement('div');
27621
+ structureInfo.className = 'pc-pane-info-list';
27622
+ this.rowCountDisplay = document.createElement('span');
27623
+ this.colCountDisplay = document.createElement('span');
27624
+ const rowInfo = document.createElement('div');
27625
+ rowInfo.className = 'pc-pane-info';
27626
+ rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27627
+ rowInfo.appendChild(this.rowCountDisplay);
27628
+ const colInfo = document.createElement('div');
27629
+ colInfo.className = 'pc-pane-info';
27630
+ colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27631
+ colInfo.appendChild(this.colCountDisplay);
27632
+ structureInfo.appendChild(rowInfo);
27633
+ structureInfo.appendChild(colInfo);
27634
+ structureSection.appendChild(structureInfo);
27635
+ // Row/column buttons
27636
+ const structureBtns = this.createButtonGroup();
27637
+ const addRowBtn = this.createButton('+ Row');
27638
+ this.addButtonListener(addRowBtn, () => this.insertRow());
27639
+ const removeRowBtn = this.createButton('- Row');
27640
+ this.addButtonListener(removeRowBtn, () => this.removeRow());
27641
+ const addColBtn = this.createButton('+ Column');
27642
+ this.addButtonListener(addColBtn, () => this.insertColumn());
27643
+ const removeColBtn = this.createButton('- Column');
27644
+ this.addButtonListener(removeColBtn, () => this.removeColumn());
27645
+ structureBtns.appendChild(addRowBtn);
27646
+ structureBtns.appendChild(removeRowBtn);
27647
+ structureBtns.appendChild(addColBtn);
27648
+ structureBtns.appendChild(removeColBtn);
27649
+ structureSection.appendChild(structureBtns);
27650
+ container.appendChild(structureSection);
27651
+ // Headers section
27652
+ const headersSection = this.createSection('Headers');
27653
+ const headerRow = this.createRow();
27654
+ this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27655
+ this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27656
+ headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27657
+ headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27658
+ headersSection.appendChild(headerRow);
27659
+ const applyHeadersBtn = this.createButton('Apply Headers');
27660
+ this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27661
+ headersSection.appendChild(applyHeadersBtn);
27662
+ container.appendChild(headersSection);
27663
+ // Defaults section
27664
+ const defaultsSection = this.createSection('Defaults');
27665
+ const defaultsRow = this.createRow();
27666
+ this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
27667
+ this.defaultBorderColorInput = this.createColorInput('#cccccc');
27668
+ defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27669
+ defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27670
+ defaultsSection.appendChild(defaultsRow);
27671
+ const applyDefaultsBtn = this.createButton('Apply Defaults');
27672
+ this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27673
+ defaultsSection.appendChild(applyDefaultsBtn);
27674
+ container.appendChild(defaultsSection);
27675
+ // Cell formatting section
27676
+ const cellSection = this.createSection('Cell Formatting');
27677
+ this.cellSelectionDisplay = this.createHint('No cell selected');
27678
+ cellSection.appendChild(this.cellSelectionDisplay);
27679
+ // Background
27680
+ this.cellBgColorInput = this.createColorInput('#ffffff');
27681
+ cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
27682
+ // Border checkboxes
27683
+ const borderChecks = document.createElement('div');
27684
+ borderChecks.className = 'pc-pane-row';
27685
+ borderChecks.style.flexWrap = 'wrap';
27686
+ borderChecks.style.gap = '4px';
27687
+ this.borderTopCheck = document.createElement('input');
27688
+ this.borderTopCheck.type = 'checkbox';
27689
+ this.borderTopCheck.checked = true;
27690
+ this.borderRightCheck = document.createElement('input');
27691
+ this.borderRightCheck.type = 'checkbox';
27692
+ this.borderRightCheck.checked = true;
27693
+ this.borderBottomCheck = document.createElement('input');
27694
+ this.borderBottomCheck.type = 'checkbox';
27695
+ this.borderBottomCheck.checked = true;
27696
+ this.borderLeftCheck = document.createElement('input');
27697
+ this.borderLeftCheck.type = 'checkbox';
27698
+ this.borderLeftCheck.checked = true;
27699
+ borderChecks.appendChild(this.createCheckbox('Top', true));
27700
+ borderChecks.appendChild(this.createCheckbox('Right', true));
27701
+ borderChecks.appendChild(this.createCheckbox('Bottom', true));
27702
+ borderChecks.appendChild(this.createCheckbox('Left', true));
27703
+ // Replace created checkboxes with our tracked ones
27704
+ const checkLabels = borderChecks.querySelectorAll('label');
27705
+ if (checkLabels[0])
27706
+ checkLabels[0].replaceChild(this.borderTopCheck, checkLabels[0].querySelector('input'));
27707
+ if (checkLabels[1])
27708
+ checkLabels[1].replaceChild(this.borderRightCheck, checkLabels[1].querySelector('input'));
27709
+ if (checkLabels[2])
27710
+ checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
27711
+ if (checkLabels[3])
27712
+ checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
27713
+ cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
27714
+ // Border properties
27715
+ const borderPropsRow = this.createRow();
27716
+ this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
27717
+ this.borderColorInput = this.createColorInput('#cccccc');
27718
+ borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27719
+ borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
27720
+ cellSection.appendChild(borderPropsRow);
27721
+ this.borderStyleSelect = this.createSelect([
27722
+ { value: 'solid', label: 'Solid' },
27723
+ { value: 'dashed', label: 'Dashed' },
27724
+ { value: 'dotted', label: 'Dotted' },
27725
+ { value: 'none', label: 'None' }
27726
+ ], 'solid');
27727
+ cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27728
+ const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
27729
+ this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
27730
+ cellSection.appendChild(applyCellBtn);
27731
+ container.appendChild(cellSection);
27732
+ return container;
27733
+ }
27734
+ updateFromFocusedTable() {
27735
+ if (!this.editor)
27736
+ return;
27737
+ const table = this.editor.getFocusedTable();
27738
+ if (table) {
27739
+ this.showTable(table);
27740
+ }
27741
+ else {
27742
+ this.hideTable();
27743
+ }
27744
+ }
27745
+ /**
27746
+ * Show the pane with the given table.
27747
+ */
27748
+ showTable(table) {
27749
+ this.currentTable = table;
27750
+ // Update structure info
27751
+ if (this.rowCountDisplay) {
27752
+ this.rowCountDisplay.textContent = String(table.rowCount);
27753
+ this.rowCountDisplay.className = 'pc-pane-info-value';
27754
+ }
27755
+ if (this.colCountDisplay) {
27756
+ this.colCountDisplay.textContent = String(table.columnCount);
27757
+ this.colCountDisplay.className = 'pc-pane-info-value';
27758
+ }
27759
+ // Update header counts
27760
+ if (this.headerRowInput) {
27761
+ this.headerRowInput.value = String(table.headerRowCount);
27762
+ }
27763
+ if (this.headerColInput) {
27764
+ this.headerColInput.value = String(table.headerColumnCount);
27765
+ }
27766
+ // Update defaults
27767
+ if (this.defaultPaddingInput) {
27768
+ this.defaultPaddingInput.value = String(table.defaultCellPadding);
27769
+ }
27770
+ if (this.defaultBorderColorInput) {
27771
+ this.defaultBorderColorInput.value = table.defaultBorderColor;
27772
+ }
27773
+ // Update cell selection info
27774
+ this.updateCellSelectionInfo(table);
27775
+ this.show();
27776
+ }
27777
+ /**
27778
+ * Hide the pane and clear current table.
27779
+ */
27780
+ hideTable() {
27781
+ this.currentTable = null;
27782
+ this.hide();
27783
+ }
27784
+ updateCellSelectionInfo(table) {
27785
+ if (!this.cellSelectionDisplay)
27786
+ return;
27787
+ const focusedCell = table.focusedCell;
27788
+ const selectedRange = table.selectedRange;
27789
+ if (selectedRange) {
27790
+ const count = (selectedRange.end.row - selectedRange.start.row + 1) *
27791
+ (selectedRange.end.col - selectedRange.start.col + 1);
27792
+ this.cellSelectionDisplay.textContent = `${count} cells selected`;
27793
+ }
27794
+ else if (focusedCell) {
27795
+ this.cellSelectionDisplay.textContent = `Cell [${focusedCell.row}, ${focusedCell.col}]`;
27796
+ // Update cell formatting controls from focused cell
27797
+ const cell = table.getCell(focusedCell.row, focusedCell.col);
27798
+ if (cell) {
27799
+ if (this.cellBgColorInput) {
27800
+ this.cellBgColorInput.value = cell.backgroundColor || '#ffffff';
27801
+ }
27802
+ // Update border controls
27803
+ const border = cell.border;
27804
+ if (this.borderTopCheck)
27805
+ this.borderTopCheck.checked = border.top.style !== 'none';
27806
+ if (this.borderRightCheck)
27807
+ this.borderRightCheck.checked = border.right.style !== 'none';
27808
+ if (this.borderBottomCheck)
27809
+ this.borderBottomCheck.checked = border.bottom.style !== 'none';
27810
+ if (this.borderLeftCheck)
27811
+ this.borderLeftCheck.checked = border.left.style !== 'none';
27812
+ // Use first active border for properties
27813
+ const activeBorder = border.top.style !== 'none' ? border.top :
27814
+ border.right.style !== 'none' ? border.right :
27815
+ border.bottom.style !== 'none' ? border.bottom :
27816
+ border.left.style !== 'none' ? border.left : border.top;
27817
+ if (this.borderWidthInput)
27818
+ this.borderWidthInput.value = String(activeBorder.width);
27819
+ if (this.borderColorInput)
27820
+ this.borderColorInput.value = activeBorder.color;
27821
+ if (this.borderStyleSelect)
27822
+ this.borderStyleSelect.value = activeBorder.style;
27823
+ }
27824
+ }
27825
+ else {
27826
+ this.cellSelectionDisplay.textContent = 'No cell selected';
27827
+ }
27828
+ }
27829
+ insertRow() {
27830
+ if (!this.editor || !this.currentTable)
27831
+ return;
27832
+ const focusedCell = this.currentTable.focusedCell;
27833
+ const rowIndex = focusedCell ? focusedCell.row + 1 : this.currentTable.rowCount;
27834
+ this.editor.tableInsertRow(this.currentTable, rowIndex);
27835
+ this.updateFromFocusedTable();
27836
+ }
27837
+ removeRow() {
27838
+ if (!this.editor || !this.currentTable)
27839
+ return;
27840
+ const focusedCell = this.currentTable.focusedCell;
27841
+ if (focusedCell && this.currentTable.rowCount > 1) {
27842
+ this.editor.tableRemoveRow(this.currentTable, focusedCell.row);
27843
+ this.updateFromFocusedTable();
27844
+ }
27845
+ }
27846
+ insertColumn() {
27847
+ if (!this.editor || !this.currentTable)
27848
+ return;
27849
+ const focusedCell = this.currentTable.focusedCell;
27850
+ const colIndex = focusedCell ? focusedCell.col + 1 : this.currentTable.columnCount;
27851
+ this.editor.tableInsertColumn(this.currentTable, colIndex);
27852
+ this.updateFromFocusedTable();
27853
+ }
27854
+ removeColumn() {
27855
+ if (!this.editor || !this.currentTable)
27856
+ return;
27857
+ const focusedCell = this.currentTable.focusedCell;
27858
+ if (focusedCell && this.currentTable.columnCount > 1) {
27859
+ this.editor.tableRemoveColumn(this.currentTable, focusedCell.col);
27860
+ this.updateFromFocusedTable();
27861
+ }
27862
+ }
27863
+ applyHeaders() {
27864
+ if (!this.currentTable)
27865
+ return;
27866
+ if (this.headerRowInput) {
27867
+ const count = parseInt(this.headerRowInput.value, 10);
27868
+ this.currentTable.setHeaderRowCount(count);
27869
+ }
27870
+ if (this.headerColInput) {
27871
+ const count = parseInt(this.headerColInput.value, 10);
27872
+ this.currentTable.setHeaderColumnCount(count);
27873
+ }
27874
+ this.editor?.render();
27875
+ this.onApplyCallback?.(true);
27876
+ }
27877
+ applyDefaults() {
27878
+ if (!this.currentTable)
27879
+ return;
27880
+ if (this.defaultPaddingInput) {
27881
+ this.currentTable.defaultCellPadding = parseInt(this.defaultPaddingInput.value, 10);
27882
+ }
27883
+ if (this.defaultBorderColorInput) {
27884
+ this.currentTable.defaultBorderColor = this.defaultBorderColorInput.value;
27885
+ }
27886
+ this.editor?.render();
27887
+ this.onApplyCallback?.(true);
27888
+ }
27889
+ applyCellFormatting() {
27890
+ if (!this.currentTable)
27891
+ return;
27892
+ const focusedCell = this.currentTable.focusedCell;
27893
+ const selectedRange = this.currentTable.selectedRange;
27894
+ // Determine cells to update
27895
+ const cells = [];
27896
+ if (selectedRange) {
27897
+ for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
27898
+ for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
27899
+ cells.push({ row, col });
27900
+ }
27901
+ }
27902
+ }
27903
+ else if (focusedCell) {
27904
+ cells.push(focusedCell);
27905
+ }
27906
+ if (cells.length === 0)
27907
+ return;
27908
+ // Build border config
27909
+ const width = parseInt(this.borderWidthInput?.value || '1', 10);
27910
+ const color = this.borderColorInput?.value || '#cccccc';
27911
+ const style = (this.borderStyleSelect?.value || 'solid');
27912
+ const borderSide = { width, color, style };
27913
+ const noneBorder = { width: 0, color: '#000000', style: 'none' };
27914
+ const border = {
27915
+ top: this.borderTopCheck?.checked ? { ...borderSide } : { ...noneBorder },
27916
+ right: this.borderRightCheck?.checked ? { ...borderSide } : { ...noneBorder },
27917
+ bottom: this.borderBottomCheck?.checked ? { ...borderSide } : { ...noneBorder },
27918
+ left: this.borderLeftCheck?.checked ? { ...borderSide } : { ...noneBorder }
27919
+ };
27920
+ const bgColor = this.cellBgColorInput?.value;
27921
+ // Apply to each cell
27922
+ for (const { row, col } of cells) {
27923
+ const cell = this.currentTable.getCell(row, col);
27924
+ if (cell) {
27925
+ if (bgColor) {
27926
+ cell.backgroundColor = bgColor;
27927
+ }
27928
+ cell.border = border;
27929
+ }
27930
+ }
27931
+ this.editor?.render();
27932
+ this.onApplyCallback?.(true);
27933
+ }
27934
+ /**
27935
+ * Get the currently focused table.
27936
+ */
27937
+ getCurrentTable() {
27938
+ return this.currentTable;
27939
+ }
27940
+ /**
27941
+ * Check if a table is currently focused.
27942
+ */
27943
+ hasTable() {
27944
+ return this.currentTable !== null;
27945
+ }
27946
+ /**
27947
+ * Update the pane from current editor state.
27948
+ */
27949
+ update() {
27950
+ this.updateFromFocusedTable();
27951
+ }
27952
+ }
27953
+
24840
27954
  exports.BaseControl = BaseControl;
24841
27955
  exports.BaseEmbeddedObject = BaseEmbeddedObject;
27956
+ exports.BasePane = BasePane;
24842
27957
  exports.BaseTextRegion = BaseTextRegion;
24843
27958
  exports.BodyTextRegion = BodyTextRegion;
24844
27959
  exports.ClipboardManager = ClipboardManager;
@@ -24846,15 +27961,21 @@ exports.ContentAnalyzer = ContentAnalyzer;
24846
27961
  exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
24847
27962
  exports.Document = Document;
24848
27963
  exports.DocumentBuilder = DocumentBuilder;
27964
+ exports.DocumentInfoPane = DocumentInfoPane;
27965
+ exports.DocumentSettingsPane = DocumentSettingsPane;
24849
27966
  exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
24850
27967
  exports.EmbeddedObjectManager = EmbeddedObjectManager;
24851
27968
  exports.EventEmitter = EventEmitter;
24852
27969
  exports.FlowingTextContent = FlowingTextContent;
24853
27970
  exports.FooterTextRegion = FooterTextRegion;
27971
+ exports.FormattingPane = FormattingPane;
24854
27972
  exports.HeaderTextRegion = HeaderTextRegion;
24855
27973
  exports.HorizontalRuler = HorizontalRuler;
24856
27974
  exports.HtmlConverter = HtmlConverter;
27975
+ exports.HyperlinkPane = HyperlinkPane;
24857
27976
  exports.ImageObject = ImageObject;
27977
+ exports.ImagePane = ImagePane;
27978
+ exports.MergeDataPane = MergeDataPane;
24858
27979
  exports.PCEditor = PCEditor;
24859
27980
  exports.PDFImportError = PDFImportError;
24860
27981
  exports.PDFImporter = PDFImporter;
@@ -24862,16 +27983,22 @@ exports.PDFParser = PDFParser;
24862
27983
  exports.Page = Page;
24863
27984
  exports.RegionManager = RegionManager;
24864
27985
  exports.RepeatingSectionManager = RepeatingSectionManager;
27986
+ exports.RepeatingSectionPane = RepeatingSectionPane;
24865
27987
  exports.RulerControl = RulerControl;
24866
27988
  exports.SubstitutionFieldManager = SubstitutionFieldManager;
27989
+ exports.SubstitutionFieldPane = SubstitutionFieldPane;
24867
27990
  exports.TableCell = TableCell;
24868
27991
  exports.TableObject = TableObject;
27992
+ exports.TablePane = TablePane;
24869
27993
  exports.TableRow = TableRow;
27994
+ exports.TableRowLoopPane = TableRowLoopPane;
24870
27995
  exports.TextBoxObject = TextBoxObject;
27996
+ exports.TextBoxPane = TextBoxPane;
24871
27997
  exports.TextFormattingManager = TextFormattingManager;
24872
27998
  exports.TextLayout = TextLayout;
24873
27999
  exports.TextMeasurer = TextMeasurer;
24874
28000
  exports.TextPositionCalculator = TextPositionCalculator;
24875
28001
  exports.TextState = TextState;
24876
28002
  exports.VerticalRuler = VerticalRuler;
28003
+ exports.ViewSettingsPane = ViewSettingsPane;
24877
28004
  //# sourceMappingURL=pc-editor.js.map