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