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