@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.
- package/README.md +1 -0
- package/dist/pc-editor.esm.js +3157 -43
- package/dist/pc-editor.esm.js.map +1 -1
- package/dist/pc-editor.js +3169 -42
- package/dist/pc-editor.js.map +1 -1
- package/dist/pc-editor.min.js +1 -1
- package/dist/pc-editor.min.js.map +1 -1
- package/dist/types/lib/core/PCEditor.d.ts +69 -0
- package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/lib/index.d.ts.map +1 -1
- package/dist/types/lib/objects/ImageObject.d.ts.map +1 -1
- package/dist/types/lib/objects/table/TableObject.d.ts +5 -0
- package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
- package/dist/types/lib/panes/BasePane.d.ts +117 -0
- package/dist/types/lib/panes/BasePane.d.ts.map +1 -0
- package/dist/types/lib/panes/DocumentInfoPane.d.ts +24 -0
- package/dist/types/lib/panes/DocumentInfoPane.d.ts.map +1 -0
- package/dist/types/lib/panes/DocumentSettingsPane.d.ts +28 -0
- package/dist/types/lib/panes/DocumentSettingsPane.d.ts.map +1 -0
- package/dist/types/lib/panes/FormattingPane.d.ts +82 -0
- package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -0
- package/dist/types/lib/panes/HyperlinkPane.d.ts +66 -0
- package/dist/types/lib/panes/HyperlinkPane.d.ts.map +1 -0
- package/dist/types/lib/panes/ImagePane.d.ts +79 -0
- package/dist/types/lib/panes/ImagePane.d.ts.map +1 -0
- package/dist/types/lib/panes/MergeDataPane.d.ts +55 -0
- package/dist/types/lib/panes/MergeDataPane.d.ts.map +1 -0
- package/dist/types/lib/panes/RepeatingSectionPane.d.ts +62 -0
- package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -0
- package/dist/types/lib/panes/SubstitutionFieldPane.d.ts +65 -0
- package/dist/types/lib/panes/SubstitutionFieldPane.d.ts.map +1 -0
- package/dist/types/lib/panes/TablePane.d.ts +88 -0
- package/dist/types/lib/panes/TablePane.d.ts.map +1 -0
- package/dist/types/lib/panes/TableRowLoopPane.d.ts +68 -0
- package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -0
- package/dist/types/lib/panes/TextBoxPane.d.ts +67 -0
- package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -0
- package/dist/types/lib/panes/ViewSettingsPane.d.ts +52 -0
- package/dist/types/lib/panes/ViewSettingsPane.d.ts.map +1 -0
- package/dist/types/lib/panes/index.d.ts +34 -0
- package/dist/types/lib/panes/index.d.ts.map +1 -0
- package/dist/types/lib/panes/types.d.ts +111 -0
- package/dist/types/lib/panes/types.d.ts.map +1 -0
- package/dist/types/lib/rendering/CanvasManager.d.ts +9 -0
- package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +16 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
- package/dist/types/lib/rendering/PDFGenerator.d.ts +16 -0
- package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
- package/dist/types/lib/text/TextFormatting.d.ts +9 -0
- package/dist/types/lib/text/TextFormatting.d.ts.map +1 -1
- package/dist/types/lib/text/TextLayout.d.ts.map +1 -1
- package/dist/types/lib/text/types.d.ts +1 -0
- package/dist/types/lib/text/types.d.ts.map +1 -1
- package/dist/types/lib/undo/transaction/MutationUndo.d.ts.map +1 -1
- 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
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
if
|
|
14326
|
-
|
|
14327
|
-
table.
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
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 +=
|
|
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.
|
|
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.
|
|
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
|
|
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
|