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