@productcloudos/editor 1.0.5 → 1.0.7
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/dist/pc-editor.esm.js +2403 -279
- package/dist/pc-editor.esm.js.map +1 -1
- package/dist/pc-editor.js +2406 -278
- 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 +89 -1
- package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
- package/dist/types/lib/fonts/FontManager.d.ts +71 -0
- package/dist/types/lib/fonts/FontManager.d.ts.map +1 -0
- package/dist/types/lib/fonts/index.d.ts +3 -0
- package/dist/types/lib/fonts/index.d.ts.map +1 -0
- package/dist/types/lib/index.d.ts +6 -4
- package/dist/types/lib/index.d.ts.map +1 -1
- package/dist/types/lib/objects/table/TableObject.d.ts +72 -1
- package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
- package/dist/types/lib/objects/table/types.d.ts +20 -0
- package/dist/types/lib/objects/table/types.d.ts.map +1 -1
- package/dist/types/lib/panes/ConditionalSectionPane.d.ts +62 -0
- package/dist/types/lib/panes/ConditionalSectionPane.d.ts.map +1 -0
- package/dist/types/lib/panes/FormattingPane.d.ts +1 -0
- package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -1
- package/dist/types/lib/panes/ImagePane.d.ts.map +1 -1
- package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -1
- package/dist/types/lib/panes/TablePane.d.ts +4 -2
- package/dist/types/lib/panes/TablePane.d.ts.map +1 -1
- package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -1
- package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -1
- package/dist/types/lib/panes/index.d.ts +2 -0
- package/dist/types/lib/panes/index.d.ts.map +1 -1
- package/dist/types/lib/rendering/CanvasManager.d.ts +2 -0
- package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +17 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
- package/dist/types/lib/rendering/PDFGenerator.d.ts +13 -0
- package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
- package/dist/types/lib/text/ConditionalSectionManager.d.ts +101 -0
- package/dist/types/lib/text/ConditionalSectionManager.d.ts.map +1 -0
- package/dist/types/lib/text/FlowingTextContent.d.ts +44 -6
- package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
- package/dist/types/lib/text/ParagraphFormatting.d.ts +1 -1
- package/dist/types/lib/text/ParagraphFormatting.d.ts.map +1 -1
- package/dist/types/lib/text/PredicateEvaluator.d.ts +23 -0
- package/dist/types/lib/text/PredicateEvaluator.d.ts.map +1 -0
- package/dist/types/lib/text/index.d.ts +3 -1
- package/dist/types/lib/text/index.d.ts.map +1 -1
- package/dist/types/lib/text/types.d.ts +21 -0
- package/dist/types/lib/text/types.d.ts.map +1 -1
- package/dist/types/lib/types/index.d.ts +13 -0
- package/dist/types/lib/types/index.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/pc-editor.esm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StandardFonts, rgb, PDFDocument } from 'pdf-lib';
|
|
2
|
+
import fontkit from '@pdf-lib/fontkit';
|
|
2
3
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
3
4
|
|
|
4
5
|
class EventEmitter {
|
|
@@ -2286,6 +2287,291 @@ class RepeatingSectionManager extends EventEmitter {
|
|
|
2286
2287
|
}
|
|
2287
2288
|
}
|
|
2288
2289
|
|
|
2290
|
+
/**
|
|
2291
|
+
* Manages conditional sections within text content.
|
|
2292
|
+
* Conditional sections define ranges of content that are shown or hidden
|
|
2293
|
+
* based on a boolean predicate evaluated against merge data.
|
|
2294
|
+
* They start and end at paragraph boundaries.
|
|
2295
|
+
*/
|
|
2296
|
+
class ConditionalSectionManager extends EventEmitter {
|
|
2297
|
+
constructor() {
|
|
2298
|
+
super();
|
|
2299
|
+
this.sections = new Map();
|
|
2300
|
+
this.nextId = 1;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Create a new conditional section.
|
|
2304
|
+
* @param startIndex Text index at paragraph start (must be 0 or immediately after a newline)
|
|
2305
|
+
* @param endIndex Text index at closing paragraph start (must be immediately after a newline)
|
|
2306
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
2307
|
+
*/
|
|
2308
|
+
create(startIndex, endIndex, predicate) {
|
|
2309
|
+
const id = `cond-${this.nextId++}`;
|
|
2310
|
+
const section = {
|
|
2311
|
+
id,
|
|
2312
|
+
predicate,
|
|
2313
|
+
startIndex,
|
|
2314
|
+
endIndex
|
|
2315
|
+
};
|
|
2316
|
+
this.sections.set(id, section);
|
|
2317
|
+
this.emit('section-added', { section });
|
|
2318
|
+
return section;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Remove a conditional section by ID.
|
|
2322
|
+
*/
|
|
2323
|
+
remove(id) {
|
|
2324
|
+
const section = this.sections.get(id);
|
|
2325
|
+
if (section) {
|
|
2326
|
+
this.sections.delete(id);
|
|
2327
|
+
this.emit('section-removed', { section });
|
|
2328
|
+
}
|
|
2329
|
+
return section;
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Get a conditional section by ID.
|
|
2333
|
+
*/
|
|
2334
|
+
getSection(id) {
|
|
2335
|
+
return this.sections.get(id);
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Get all conditional sections.
|
|
2339
|
+
*/
|
|
2340
|
+
getSections() {
|
|
2341
|
+
return Array.from(this.sections.values());
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Get all conditional sections sorted by startIndex.
|
|
2345
|
+
*/
|
|
2346
|
+
getSectionsSorted() {
|
|
2347
|
+
return this.getSections().sort((a, b) => a.startIndex - b.startIndex);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Get all conditional sections sorted by startIndex in descending order.
|
|
2351
|
+
* Useful for processing sections end-to-start during merge.
|
|
2352
|
+
*/
|
|
2353
|
+
getSectionsDescending() {
|
|
2354
|
+
return this.getSections().sort((a, b) => b.startIndex - a.startIndex);
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Find a conditional section that contains the given text index.
|
|
2358
|
+
*/
|
|
2359
|
+
getSectionContaining(textIndex) {
|
|
2360
|
+
for (const section of this.sections.values()) {
|
|
2361
|
+
if (textIndex >= section.startIndex && textIndex < section.endIndex) {
|
|
2362
|
+
return section;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return undefined;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
2369
|
+
* Returns the section if textIndex matches startIndex or endIndex.
|
|
2370
|
+
*/
|
|
2371
|
+
getSectionAtBoundary(textIndex) {
|
|
2372
|
+
for (const section of this.sections.values()) {
|
|
2373
|
+
if (section.startIndex === textIndex || section.endIndex === textIndex) {
|
|
2374
|
+
return section;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return undefined;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Update a section's predicate.
|
|
2381
|
+
*/
|
|
2382
|
+
updatePredicate(id, predicate) {
|
|
2383
|
+
const section = this.sections.get(id);
|
|
2384
|
+
if (!section) {
|
|
2385
|
+
return false;
|
|
2386
|
+
}
|
|
2387
|
+
section.predicate = predicate;
|
|
2388
|
+
this.emit('section-updated', { section });
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Update a section's visual state (called during rendering).
|
|
2393
|
+
*/
|
|
2394
|
+
updateVisualState(id, visualState) {
|
|
2395
|
+
const section = this.sections.get(id);
|
|
2396
|
+
if (!section) {
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
section.visualState = visualState;
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Shift section positions when text is inserted.
|
|
2404
|
+
* @param fromIndex The position where text was inserted
|
|
2405
|
+
* @param delta The number of characters inserted (positive)
|
|
2406
|
+
*/
|
|
2407
|
+
shiftSections(fromIndex, delta) {
|
|
2408
|
+
let changed = false;
|
|
2409
|
+
for (const section of this.sections.values()) {
|
|
2410
|
+
if (fromIndex <= section.startIndex) {
|
|
2411
|
+
section.startIndex += delta;
|
|
2412
|
+
section.endIndex += delta;
|
|
2413
|
+
changed = true;
|
|
2414
|
+
}
|
|
2415
|
+
else if (fromIndex < section.endIndex) {
|
|
2416
|
+
section.endIndex += delta;
|
|
2417
|
+
changed = true;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (changed) {
|
|
2421
|
+
this.emit('sections-shifted', { fromIndex, delta });
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Handle deletion of text range.
|
|
2426
|
+
* Sections entirely within the deleted range are removed.
|
|
2427
|
+
* Sections partially overlapping are adjusted or removed.
|
|
2428
|
+
* @returns Array of removed sections
|
|
2429
|
+
*/
|
|
2430
|
+
handleDeletion(start, length) {
|
|
2431
|
+
const end = start + length;
|
|
2432
|
+
const removedSections = [];
|
|
2433
|
+
const sectionsToUpdate = [];
|
|
2434
|
+
for (const section of this.sections.values()) {
|
|
2435
|
+
if (section.startIndex >= start && section.endIndex <= end) {
|
|
2436
|
+
removedSections.push(section);
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (section.startIndex < end && section.endIndex > start) {
|
|
2440
|
+
if (start <= section.startIndex) {
|
|
2441
|
+
removedSections.push(section);
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
if (start < section.endIndex) {
|
|
2445
|
+
if (end >= section.endIndex) {
|
|
2446
|
+
const newEnd = start;
|
|
2447
|
+
if (newEnd <= section.startIndex) {
|
|
2448
|
+
removedSections.push(section);
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
sectionsToUpdate.push({
|
|
2452
|
+
id: section.id,
|
|
2453
|
+
newStart: section.startIndex,
|
|
2454
|
+
newEnd: newEnd
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
const newEnd = section.endIndex - length;
|
|
2459
|
+
sectionsToUpdate.push({
|
|
2460
|
+
id: section.id,
|
|
2461
|
+
newStart: section.startIndex,
|
|
2462
|
+
newEnd: newEnd
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (section.startIndex >= end) {
|
|
2469
|
+
sectionsToUpdate.push({
|
|
2470
|
+
id: section.id,
|
|
2471
|
+
newStart: section.startIndex - length,
|
|
2472
|
+
newEnd: section.endIndex - length
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
for (const section of removedSections) {
|
|
2477
|
+
this.sections.delete(section.id);
|
|
2478
|
+
this.emit('section-removed', { section });
|
|
2479
|
+
}
|
|
2480
|
+
for (const update of sectionsToUpdate) {
|
|
2481
|
+
const section = this.sections.get(update.id);
|
|
2482
|
+
if (section) {
|
|
2483
|
+
section.startIndex = update.newStart;
|
|
2484
|
+
section.endIndex = update.newEnd;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (removedSections.length > 0 || sectionsToUpdate.length > 0) {
|
|
2488
|
+
this.emit('sections-changed');
|
|
2489
|
+
}
|
|
2490
|
+
return removedSections;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Validate that the given boundaries are at paragraph boundaries.
|
|
2494
|
+
* Also checks that conditionals don't partially overlap repeating sections.
|
|
2495
|
+
* @param start The proposed start index
|
|
2496
|
+
* @param end The proposed end index
|
|
2497
|
+
* @param content The text content to validate against
|
|
2498
|
+
* @returns true if valid, false otherwise
|
|
2499
|
+
*/
|
|
2500
|
+
validateBoundaries(start, end, content) {
|
|
2501
|
+
if (start !== 0 && content[start - 1] !== '\n') {
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
if (end !== 0 && end < content.length && content[end - 1] !== '\n') {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
if (end <= start) {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
// Check for overlapping conditional sections
|
|
2511
|
+
for (const existing of this.sections.values()) {
|
|
2512
|
+
if ((start >= existing.startIndex && start < existing.endIndex) ||
|
|
2513
|
+
(end > existing.startIndex && end <= existing.endIndex) ||
|
|
2514
|
+
(start <= existing.startIndex && end >= existing.endIndex)) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Get the number of conditional sections.
|
|
2522
|
+
*/
|
|
2523
|
+
get count() {
|
|
2524
|
+
return this.sections.size;
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Check if there are any conditional sections.
|
|
2528
|
+
*/
|
|
2529
|
+
get isEmpty() {
|
|
2530
|
+
return this.sections.size === 0;
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Clear all conditional sections.
|
|
2534
|
+
*/
|
|
2535
|
+
clear() {
|
|
2536
|
+
const hadSections = this.sections.size > 0;
|
|
2537
|
+
this.sections.clear();
|
|
2538
|
+
if (hadSections) {
|
|
2539
|
+
this.emit('sections-cleared');
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Serialize all sections to JSON.
|
|
2544
|
+
*/
|
|
2545
|
+
toJSON() {
|
|
2546
|
+
return this.getSectionsSorted().map(section => ({
|
|
2547
|
+
id: section.id,
|
|
2548
|
+
predicate: section.predicate,
|
|
2549
|
+
startIndex: section.startIndex,
|
|
2550
|
+
endIndex: section.endIndex
|
|
2551
|
+
}));
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Load sections from serialized data.
|
|
2555
|
+
*/
|
|
2556
|
+
fromJSON(data) {
|
|
2557
|
+
this.clear();
|
|
2558
|
+
for (const sectionData of data) {
|
|
2559
|
+
const section = {
|
|
2560
|
+
id: sectionData.id,
|
|
2561
|
+
predicate: sectionData.predicate,
|
|
2562
|
+
startIndex: sectionData.startIndex,
|
|
2563
|
+
endIndex: sectionData.endIndex
|
|
2564
|
+
};
|
|
2565
|
+
this.sections.set(section.id, section);
|
|
2566
|
+
const idNum = parseInt(sectionData.id.replace('cond-', ''), 10);
|
|
2567
|
+
if (!isNaN(idNum) && idNum >= this.nextId) {
|
|
2568
|
+
this.nextId = idNum + 1;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
this.emit('sections-loaded', { count: this.sections.size });
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2289
2575
|
/**
|
|
2290
2576
|
* HyperlinkManager - Manages hyperlinks within flowing text content
|
|
2291
2577
|
*/
|
|
@@ -6191,12 +6477,20 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6191
6477
|
this._coveredCells = new Map();
|
|
6192
6478
|
// Row loops for merge expansion
|
|
6193
6479
|
this._rowLoops = new Map();
|
|
6480
|
+
// Row conditionals for conditional display
|
|
6481
|
+
this._rowConditionals = new Map();
|
|
6194
6482
|
// Layout caching for performance
|
|
6195
6483
|
this._layoutDirty = true;
|
|
6196
6484
|
this._cachedRowHeights = [];
|
|
6197
6485
|
this._cachedRowPositions = [];
|
|
6198
6486
|
// Multi-page rendering info: pageIndex -> slice render info
|
|
6199
6487
|
this._renderedSlices = new Map();
|
|
6488
|
+
// ============================================
|
|
6489
|
+
// Table Row Conditionals
|
|
6490
|
+
// ============================================
|
|
6491
|
+
this._nextCondId = 1;
|
|
6492
|
+
this._selectedRowLoopId = null;
|
|
6493
|
+
this._selectedRowConditionalId = null;
|
|
6200
6494
|
// Tables ONLY support block positioning - force it regardless of config
|
|
6201
6495
|
this._position = 'block';
|
|
6202
6496
|
// Initialize defaults
|
|
@@ -6890,6 +7184,85 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6890
7184
|
loopRangesOverlap(start1, end1, start2, end2) {
|
|
6891
7185
|
return start1 <= end2 && start2 <= end1;
|
|
6892
7186
|
}
|
|
7187
|
+
generateRowConditionalId() {
|
|
7188
|
+
return `row-cond-${this._nextCondId++}`;
|
|
7189
|
+
}
|
|
7190
|
+
/**
|
|
7191
|
+
* Create a row conditional.
|
|
7192
|
+
*/
|
|
7193
|
+
createRowConditional(startRowIndex, endRowIndex, predicate) {
|
|
7194
|
+
if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
|
|
7195
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Invalid row range');
|
|
7196
|
+
return null;
|
|
7197
|
+
}
|
|
7198
|
+
if (startRowIndex > endRowIndex) {
|
|
7199
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Start index must be <= end index');
|
|
7200
|
+
return null;
|
|
7201
|
+
}
|
|
7202
|
+
// Check for overlapping conditionals
|
|
7203
|
+
for (const existing of this._rowConditionals.values()) {
|
|
7204
|
+
if (this.loopRangesOverlap(startRowIndex, endRowIndex, existing.startRowIndex, existing.endRowIndex)) {
|
|
7205
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Range overlaps with existing conditional');
|
|
7206
|
+
return null;
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
const cond = {
|
|
7210
|
+
id: this.generateRowConditionalId(),
|
|
7211
|
+
predicate,
|
|
7212
|
+
startRowIndex,
|
|
7213
|
+
endRowIndex
|
|
7214
|
+
};
|
|
7215
|
+
this._rowConditionals.set(cond.id, cond);
|
|
7216
|
+
this.emit('row-conditional-created', { conditional: cond });
|
|
7217
|
+
this.emit('content-changed', {});
|
|
7218
|
+
return cond;
|
|
7219
|
+
}
|
|
7220
|
+
/**
|
|
7221
|
+
* Remove a row conditional by ID.
|
|
7222
|
+
*/
|
|
7223
|
+
removeRowConditional(id) {
|
|
7224
|
+
const cond = this._rowConditionals.get(id);
|
|
7225
|
+
if (!cond)
|
|
7226
|
+
return false;
|
|
7227
|
+
this._rowConditionals.delete(id);
|
|
7228
|
+
this.emit('row-conditional-removed', { conditionalId: id });
|
|
7229
|
+
return true;
|
|
7230
|
+
}
|
|
7231
|
+
/**
|
|
7232
|
+
* Get a row conditional by ID.
|
|
7233
|
+
*/
|
|
7234
|
+
getRowConditional(id) {
|
|
7235
|
+
return this._rowConditionals.get(id);
|
|
7236
|
+
}
|
|
7237
|
+
/**
|
|
7238
|
+
* Get all row conditionals.
|
|
7239
|
+
*/
|
|
7240
|
+
getAllRowConditionals() {
|
|
7241
|
+
return Array.from(this._rowConditionals.values());
|
|
7242
|
+
}
|
|
7243
|
+
/**
|
|
7244
|
+
* Get the row conditional at a given row index.
|
|
7245
|
+
*/
|
|
7246
|
+
getRowConditionalAtRow(rowIndex) {
|
|
7247
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7248
|
+
if (rowIndex >= cond.startRowIndex && rowIndex <= cond.endRowIndex) {
|
|
7249
|
+
return cond;
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
return undefined;
|
|
7253
|
+
}
|
|
7254
|
+
/**
|
|
7255
|
+
* Update a row conditional's predicate.
|
|
7256
|
+
*/
|
|
7257
|
+
updateRowConditionalPredicate(id, predicate) {
|
|
7258
|
+
const cond = this._rowConditionals.get(id);
|
|
7259
|
+
if (!cond)
|
|
7260
|
+
return false;
|
|
7261
|
+
cond.predicate = predicate;
|
|
7262
|
+
this.emit('row-conditional-updated', { conditional: cond });
|
|
7263
|
+
this.emit('content-changed', {});
|
|
7264
|
+
return true;
|
|
7265
|
+
}
|
|
6893
7266
|
/**
|
|
6894
7267
|
* Shift row loop indices when rows are inserted or removed.
|
|
6895
7268
|
* @param fromIndex The row index where insertion/removal occurred
|
|
@@ -6930,6 +7303,41 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6930
7303
|
this._rowLoops.delete(id);
|
|
6931
7304
|
this.emit('row-loop-removed', { loopId: id, reason: 'row-deleted' });
|
|
6932
7305
|
}
|
|
7306
|
+
// Also shift row conditional indices
|
|
7307
|
+
this.shiftRowConditionalIndices(fromIndex, delta);
|
|
7308
|
+
}
|
|
7309
|
+
/**
|
|
7310
|
+
* Shift row conditional indices when rows are inserted or removed.
|
|
7311
|
+
*/
|
|
7312
|
+
shiftRowConditionalIndices(fromIndex, delta) {
|
|
7313
|
+
const condsToRemove = [];
|
|
7314
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7315
|
+
if (delta < 0) {
|
|
7316
|
+
const removeCount = Math.abs(delta);
|
|
7317
|
+
const removeEnd = fromIndex + removeCount - 1;
|
|
7318
|
+
if (fromIndex <= cond.endRowIndex && removeEnd >= cond.startRowIndex) {
|
|
7319
|
+
condsToRemove.push(cond.id);
|
|
7320
|
+
continue;
|
|
7321
|
+
}
|
|
7322
|
+
if (fromIndex < cond.startRowIndex) {
|
|
7323
|
+
cond.startRowIndex += delta;
|
|
7324
|
+
cond.endRowIndex += delta;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
else {
|
|
7328
|
+
if (fromIndex <= cond.startRowIndex) {
|
|
7329
|
+
cond.startRowIndex += delta;
|
|
7330
|
+
cond.endRowIndex += delta;
|
|
7331
|
+
}
|
|
7332
|
+
else if (fromIndex <= cond.endRowIndex) {
|
|
7333
|
+
cond.endRowIndex += delta;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
for (const id of condsToRemove) {
|
|
7338
|
+
this._rowConditionals.delete(id);
|
|
7339
|
+
this.emit('row-conditional-removed', { conditionalId: id, reason: 'row-deleted' });
|
|
7340
|
+
}
|
|
6933
7341
|
}
|
|
6934
7342
|
/**
|
|
6935
7343
|
* Get rows in a range (for loop expansion).
|
|
@@ -7737,6 +8145,9 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7737
8145
|
result.mergedCell.markReflowDirty();
|
|
7738
8146
|
}
|
|
7739
8147
|
this.clearSelection();
|
|
8148
|
+
// Focus the anchor (top-left) cell of the merged range
|
|
8149
|
+
const normalized = TableCellMerger.normalizeRange(mergeRange);
|
|
8150
|
+
this.focusCell(normalized.start.row, normalized.start.col);
|
|
7740
8151
|
this.emit('cells-merged', { range: mergeRange });
|
|
7741
8152
|
this.emit('content-changed', {});
|
|
7742
8153
|
}
|
|
@@ -7827,6 +8238,10 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7827
8238
|
if (this._rowLoops.size > 0) {
|
|
7828
8239
|
this.renderRowLoopIndicators(ctx);
|
|
7829
8240
|
}
|
|
8241
|
+
// Render row conditional indicators
|
|
8242
|
+
if (this._rowConditionals.size > 0) {
|
|
8243
|
+
this.renderRowConditionalIndicators(ctx);
|
|
8244
|
+
}
|
|
7830
8245
|
// Render cell range selection highlight
|
|
7831
8246
|
if (this._selectedRange) {
|
|
7832
8247
|
this.renderRangeSelection(ctx);
|
|
@@ -7841,11 +8256,54 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7841
8256
|
}
|
|
7842
8257
|
}
|
|
7843
8258
|
/**
|
|
7844
|
-
*
|
|
8259
|
+
* Select a row loop by ID (for pane display).
|
|
7845
8260
|
*/
|
|
8261
|
+
selectRowLoop(loopId) {
|
|
8262
|
+
this._selectedRowLoopId = loopId;
|
|
8263
|
+
}
|
|
8264
|
+
/**
|
|
8265
|
+
* Get the currently selected row loop ID.
|
|
8266
|
+
*/
|
|
8267
|
+
get selectedRowLoopId() {
|
|
8268
|
+
return this._selectedRowLoopId;
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Hit-test a point against row loop labels.
|
|
8272
|
+
* Point should be in table-local coordinates.
|
|
8273
|
+
* Returns the loop if a label was clicked, null otherwise.
|
|
8274
|
+
*/
|
|
8275
|
+
getRowLoopAtPoint(point) {
|
|
8276
|
+
let rowPositions = this._cachedRowPositions;
|
|
8277
|
+
if (rowPositions.length === 0) {
|
|
8278
|
+
rowPositions = [];
|
|
8279
|
+
let y = 0;
|
|
8280
|
+
for (const row of this._rows) {
|
|
8281
|
+
rowPositions.push(y);
|
|
8282
|
+
y += row.calculatedHeight;
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
for (const loop of this._rowLoops.values()) {
|
|
8286
|
+
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
8287
|
+
let _endY = startY;
|
|
8288
|
+
for (let i = loop.startRowIndex; i <= loop.endRowIndex && i < this._rows.length; i++) {
|
|
8289
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8290
|
+
}
|
|
8291
|
+
// Label bounds (matches rendering)
|
|
8292
|
+
const labelWidth = 30; // approximate for "Loop" at 10px
|
|
8293
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8294
|
+
const labelX = -6 - labelWidth - 4;
|
|
8295
|
+
const labelY = startY - labelHeight - 2;
|
|
8296
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8297
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8298
|
+
return loop;
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
return null;
|
|
8302
|
+
}
|
|
7846
8303
|
renderRowLoopIndicators(ctx) {
|
|
7847
|
-
const
|
|
7848
|
-
const
|
|
8304
|
+
const color = TableObject.LOOP_COLOR;
|
|
8305
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8306
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
7849
8307
|
// Calculate row Y positions if not cached
|
|
7850
8308
|
let rowPositions = this._cachedRowPositions;
|
|
7851
8309
|
if (rowPositions.length === 0) {
|
|
@@ -7856,12 +8314,8 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7856
8314
|
y += row.calculatedHeight;
|
|
7857
8315
|
}
|
|
7858
8316
|
}
|
|
7859
|
-
// Colors for different loops (cycle through these)
|
|
7860
|
-
const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
|
|
7861
|
-
let colorIndex = 0;
|
|
7862
8317
|
for (const loop of this._rowLoops.values()) {
|
|
7863
|
-
const
|
|
7864
|
-
colorIndex++;
|
|
8318
|
+
const isSelected = this._selectedRowLoopId === loop.id;
|
|
7865
8319
|
// Calculate the Y range for this loop
|
|
7866
8320
|
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
7867
8321
|
let endY = startY;
|
|
@@ -7871,31 +8325,149 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7871
8325
|
const loopHeight = endY - startY;
|
|
7872
8326
|
// Draw colored stripe on left side
|
|
7873
8327
|
ctx.fillStyle = color;
|
|
7874
|
-
ctx.fillRect(-
|
|
7875
|
-
// Draw
|
|
8328
|
+
ctx.fillRect(-6, startY, 4, loopHeight);
|
|
8329
|
+
// Draw vertical connector line
|
|
8330
|
+
ctx.strokeStyle = color;
|
|
8331
|
+
ctx.lineWidth = 1;
|
|
8332
|
+
ctx.beginPath();
|
|
8333
|
+
ctx.moveTo(-4, startY);
|
|
8334
|
+
ctx.lineTo(-4, endY);
|
|
8335
|
+
ctx.stroke();
|
|
8336
|
+
// Draw "Loop" label — matches text flow style
|
|
7876
8337
|
ctx.save();
|
|
7877
8338
|
ctx.font = '10px Arial';
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
const
|
|
7881
|
-
const
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
ctx.
|
|
8339
|
+
const labelText = 'Loop';
|
|
8340
|
+
const metrics = ctx.measureText(labelText);
|
|
8341
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8342
|
+
const boxHeight = 10 + padding * 2;
|
|
8343
|
+
const labelX = -6 - boxWidth - 4;
|
|
8344
|
+
const labelY = startY - boxHeight - 2;
|
|
8345
|
+
ctx.beginPath();
|
|
8346
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8347
|
+
if (isSelected) {
|
|
8348
|
+
// Selected: filled background with white text
|
|
8349
|
+
ctx.fillStyle = color;
|
|
8350
|
+
ctx.fill();
|
|
8351
|
+
ctx.fillStyle = '#ffffff';
|
|
8352
|
+
}
|
|
8353
|
+
else {
|
|
8354
|
+
// Not selected: white background, outlined with colored text
|
|
8355
|
+
ctx.fillStyle = '#ffffff';
|
|
8356
|
+
ctx.fill();
|
|
8357
|
+
ctx.strokeStyle = color;
|
|
8358
|
+
ctx.lineWidth = 1.5;
|
|
8359
|
+
ctx.stroke();
|
|
8360
|
+
ctx.fillStyle = color;
|
|
8361
|
+
}
|
|
8362
|
+
ctx.textBaseline = 'middle';
|
|
8363
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
7885
8364
|
ctx.restore();
|
|
7886
|
-
|
|
8365
|
+
}
|
|
8366
|
+
}
|
|
8367
|
+
/**
|
|
8368
|
+
* Select a row conditional by ID (for pane display).
|
|
8369
|
+
*/
|
|
8370
|
+
selectRowConditional(conditionalId) {
|
|
8371
|
+
this._selectedRowConditionalId = conditionalId;
|
|
8372
|
+
}
|
|
8373
|
+
/**
|
|
8374
|
+
* Get the currently selected row conditional ID.
|
|
8375
|
+
*/
|
|
8376
|
+
get selectedRowConditionalId() {
|
|
8377
|
+
return this._selectedRowConditionalId;
|
|
8378
|
+
}
|
|
8379
|
+
/**
|
|
8380
|
+
* Hit-test a point against row conditional labels.
|
|
8381
|
+
* Point should be in table-local coordinates.
|
|
8382
|
+
*/
|
|
8383
|
+
getRowConditionalAtPoint(point) {
|
|
8384
|
+
let rowPositions = this._cachedRowPositions;
|
|
8385
|
+
if (rowPositions.length === 0) {
|
|
8386
|
+
rowPositions = [];
|
|
8387
|
+
let y = 0;
|
|
8388
|
+
for (const row of this._rows) {
|
|
8389
|
+
rowPositions.push(y);
|
|
8390
|
+
y += row.calculatedHeight;
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8394
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8395
|
+
let _endY = startY;
|
|
8396
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8397
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8398
|
+
}
|
|
8399
|
+
// Label bounds (right side of table, offset from loop labels)
|
|
8400
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8401
|
+
const labelWidth = 22; // approximate for "If" at 10px
|
|
8402
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8403
|
+
const labelX = totalWidth + 10;
|
|
8404
|
+
const labelY = startY - labelHeight - 2;
|
|
8405
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8406
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8407
|
+
return cond;
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
return null;
|
|
8411
|
+
}
|
|
8412
|
+
renderRowConditionalIndicators(ctx) {
|
|
8413
|
+
const color = TableObject.COND_COLOR;
|
|
8414
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8415
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
8416
|
+
let rowPositions = this._cachedRowPositions;
|
|
8417
|
+
if (rowPositions.length === 0) {
|
|
8418
|
+
rowPositions = [];
|
|
8419
|
+
let y = 0;
|
|
8420
|
+
for (const row of this._rows) {
|
|
8421
|
+
rowPositions.push(y);
|
|
8422
|
+
y += row.calculatedHeight;
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8426
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8427
|
+
const isSelected = this._selectedRowConditionalId === cond.id;
|
|
8428
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8429
|
+
let endY = startY;
|
|
8430
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8431
|
+
endY += this._rows[i].calculatedHeight;
|
|
8432
|
+
}
|
|
8433
|
+
const condHeight = endY - startY;
|
|
8434
|
+
// Draw colored stripe on right side
|
|
8435
|
+
ctx.fillStyle = color;
|
|
8436
|
+
ctx.fillRect(totalWidth + 2, startY, 4, condHeight);
|
|
8437
|
+
// Draw vertical connector line
|
|
7887
8438
|
ctx.strokeStyle = color;
|
|
7888
8439
|
ctx.lineWidth = 1;
|
|
7889
8440
|
ctx.beginPath();
|
|
7890
|
-
|
|
7891
|
-
ctx.
|
|
7892
|
-
ctx.lineTo(-indicatorWidth - 6, startY);
|
|
7893
|
-
ctx.lineTo(-indicatorWidth - 6, startY + 6);
|
|
7894
|
-
// Bottom bracket
|
|
7895
|
-
ctx.moveTo(-indicatorWidth - 2, endY);
|
|
7896
|
-
ctx.lineTo(-indicatorWidth - 6, endY);
|
|
7897
|
-
ctx.lineTo(-indicatorWidth - 6, endY - 6);
|
|
8441
|
+
ctx.moveTo(totalWidth + 4, startY);
|
|
8442
|
+
ctx.lineTo(totalWidth + 4, endY);
|
|
7898
8443
|
ctx.stroke();
|
|
8444
|
+
// Draw "If" label
|
|
8445
|
+
ctx.save();
|
|
8446
|
+
ctx.font = '10px Arial';
|
|
8447
|
+
const labelText = 'If';
|
|
8448
|
+
const metrics = ctx.measureText(labelText);
|
|
8449
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8450
|
+
const boxHeight = 10 + padding * 2;
|
|
8451
|
+
const labelX = totalWidth + 10;
|
|
8452
|
+
const labelY = startY - boxHeight - 2;
|
|
8453
|
+
ctx.beginPath();
|
|
8454
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8455
|
+
if (isSelected) {
|
|
8456
|
+
ctx.fillStyle = color;
|
|
8457
|
+
ctx.fill();
|
|
8458
|
+
ctx.fillStyle = '#ffffff';
|
|
8459
|
+
}
|
|
8460
|
+
else {
|
|
8461
|
+
ctx.fillStyle = '#ffffff';
|
|
8462
|
+
ctx.fill();
|
|
8463
|
+
ctx.strokeStyle = color;
|
|
8464
|
+
ctx.lineWidth = 1.5;
|
|
8465
|
+
ctx.stroke();
|
|
8466
|
+
ctx.fillStyle = color;
|
|
8467
|
+
}
|
|
8468
|
+
ctx.textBaseline = 'middle';
|
|
8469
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
8470
|
+
ctx.restore();
|
|
7899
8471
|
}
|
|
7900
8472
|
}
|
|
7901
8473
|
/**
|
|
@@ -8014,6 +8586,14 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8014
8586
|
columns: this._columns.map(col => ({ ...col })),
|
|
8015
8587
|
rows: this._rows.map(row => row.toData()),
|
|
8016
8588
|
rowLoops,
|
|
8589
|
+
rowConditionals: this._rowConditionals.size > 0
|
|
8590
|
+
? Array.from(this._rowConditionals.values()).map(c => ({
|
|
8591
|
+
id: c.id,
|
|
8592
|
+
predicate: c.predicate,
|
|
8593
|
+
startRowIndex: c.startRowIndex,
|
|
8594
|
+
endRowIndex: c.endRowIndex
|
|
8595
|
+
}))
|
|
8596
|
+
: undefined,
|
|
8017
8597
|
defaultCellPadding: this._defaultCellPadding,
|
|
8018
8598
|
defaultBorderColor: this._defaultBorderColor,
|
|
8019
8599
|
defaultBorderWidth: this._defaultBorderWidth,
|
|
@@ -8057,6 +8637,17 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8057
8637
|
});
|
|
8058
8638
|
}
|
|
8059
8639
|
}
|
|
8640
|
+
// Load row conditionals if present
|
|
8641
|
+
if (data.data.rowConditionals) {
|
|
8642
|
+
for (const condData of data.data.rowConditionals) {
|
|
8643
|
+
table._rowConditionals.set(condData.id, {
|
|
8644
|
+
id: condData.id,
|
|
8645
|
+
predicate: condData.predicate,
|
|
8646
|
+
startRowIndex: condData.startRowIndex,
|
|
8647
|
+
endRowIndex: condData.endRowIndex
|
|
8648
|
+
});
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
8060
8651
|
table.updateCoveredCells();
|
|
8061
8652
|
return table;
|
|
8062
8653
|
}
|
|
@@ -8086,6 +8677,18 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8086
8677
|
});
|
|
8087
8678
|
}
|
|
8088
8679
|
}
|
|
8680
|
+
// Restore row conditionals if any
|
|
8681
|
+
this._rowConditionals.clear();
|
|
8682
|
+
if (data.data.rowConditionals) {
|
|
8683
|
+
for (const condData of data.data.rowConditionals) {
|
|
8684
|
+
this._rowConditionals.set(condData.id, {
|
|
8685
|
+
id: condData.id,
|
|
8686
|
+
predicate: condData.predicate,
|
|
8687
|
+
startRowIndex: condData.startRowIndex,
|
|
8688
|
+
endRowIndex: condData.endRowIndex
|
|
8689
|
+
});
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8089
8692
|
// Restore defaults
|
|
8090
8693
|
if (data.data.defaultCellPadding !== undefined) {
|
|
8091
8694
|
this._defaultCellPadding = data.data.defaultCellPadding;
|
|
@@ -8105,6 +8708,13 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8105
8708
|
return TableObject.fromData(this.toData());
|
|
8106
8709
|
}
|
|
8107
8710
|
}
|
|
8711
|
+
/**
|
|
8712
|
+
* Render row loop indicators (colored stripe on left side of loop rows).
|
|
8713
|
+
*/
|
|
8714
|
+
TableObject.LOOP_COLOR = '#6B46C1';
|
|
8715
|
+
TableObject.LOOP_LABEL_PADDING = 4;
|
|
8716
|
+
TableObject.LOOP_LABEL_RADIUS = 4;
|
|
8717
|
+
TableObject.COND_COLOR = '#D97706'; // Orange
|
|
8108
8718
|
|
|
8109
8719
|
/**
|
|
8110
8720
|
* TableResizeHandler - Handles column and row resize operations for tables.
|
|
@@ -8490,6 +9100,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8490
9100
|
this.substitutionFields = new SubstitutionFieldManager();
|
|
8491
9101
|
this.embeddedObjects = new EmbeddedObjectManager();
|
|
8492
9102
|
this.repeatingSections = new RepeatingSectionManager();
|
|
9103
|
+
this.conditionalSections = new ConditionalSectionManager();
|
|
8493
9104
|
this.hyperlinks = new HyperlinkManager();
|
|
8494
9105
|
this.layout = new TextLayout();
|
|
8495
9106
|
this.setupEventForwarding();
|
|
@@ -8527,6 +9138,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8527
9138
|
this.substitutionFields.handleDeletion(data.start, data.length);
|
|
8528
9139
|
this.embeddedObjects.handleDeletion(data.start, data.length);
|
|
8529
9140
|
this.repeatingSections.handleDeletion(data.start, data.length);
|
|
9141
|
+
this.conditionalSections.handleDeletion(data.start, data.length);
|
|
8530
9142
|
this.paragraphFormatting.handleDeletion(data.start, data.length);
|
|
8531
9143
|
this.hyperlinks.handleDeletion(data.start, data.length);
|
|
8532
9144
|
this.emit('content-changed', {
|
|
@@ -8578,6 +9190,16 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8578
9190
|
this.repeatingSections.on('section-updated', (data) => {
|
|
8579
9191
|
this.emit('repeating-section-updated', data);
|
|
8580
9192
|
});
|
|
9193
|
+
// Forward conditional section events
|
|
9194
|
+
this.conditionalSections.on('section-added', (data) => {
|
|
9195
|
+
this.emit('conditional-section-added', data);
|
|
9196
|
+
});
|
|
9197
|
+
this.conditionalSections.on('section-removed', (data) => {
|
|
9198
|
+
this.emit('conditional-section-removed', data);
|
|
9199
|
+
});
|
|
9200
|
+
this.conditionalSections.on('section-updated', (data) => {
|
|
9201
|
+
this.emit('conditional-section-updated', data);
|
|
9202
|
+
});
|
|
8581
9203
|
// Forward hyperlink events
|
|
8582
9204
|
this.hyperlinks.on('hyperlink-added', (data) => {
|
|
8583
9205
|
this.emit('hyperlink-added', data);
|
|
@@ -8635,6 +9257,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8635
9257
|
this.substitutionFields.shiftFields(insertAt, text.length);
|
|
8636
9258
|
this.embeddedObjects.shiftObjects(insertAt, text.length);
|
|
8637
9259
|
this.repeatingSections.shiftSections(insertAt, text.length);
|
|
9260
|
+
this.conditionalSections.shiftSections(insertAt, text.length);
|
|
8638
9261
|
this.hyperlinks.shiftHyperlinks(insertAt, text.length);
|
|
8639
9262
|
// Insert the text first so we have the full content
|
|
8640
9263
|
this.textState.insertText(text, insertAt);
|
|
@@ -8707,6 +9330,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8707
9330
|
this.substitutionFields.shiftFields(position, text.length);
|
|
8708
9331
|
this.embeddedObjects.shiftObjects(position, text.length);
|
|
8709
9332
|
this.repeatingSections.shiftSections(position, text.length);
|
|
9333
|
+
this.conditionalSections.shiftSections(position, text.length);
|
|
8710
9334
|
this.hyperlinks.shiftHyperlinks(position, text.length);
|
|
8711
9335
|
// Insert the text
|
|
8712
9336
|
const content = this.textState.getText();
|
|
@@ -8724,6 +9348,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8724
9348
|
this.substitutionFields.handleDeletion(position, length);
|
|
8725
9349
|
this.embeddedObjects.handleDeletion(position, length);
|
|
8726
9350
|
this.repeatingSections.handleDeletion(position, length);
|
|
9351
|
+
this.conditionalSections.handleDeletion(position, length);
|
|
8727
9352
|
this.paragraphFormatting.handleDeletion(position, length);
|
|
8728
9353
|
this.hyperlinks.handleDeletion(position, length);
|
|
8729
9354
|
// Delete the text
|
|
@@ -9113,6 +9738,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9113
9738
|
this.substitutionFields.shiftFields(insertAt, 1);
|
|
9114
9739
|
this.embeddedObjects.shiftObjects(insertAt, 1);
|
|
9115
9740
|
this.repeatingSections.shiftSections(insertAt, 1);
|
|
9741
|
+
this.conditionalSections.shiftSections(insertAt, 1);
|
|
9116
9742
|
// Insert the placeholder character
|
|
9117
9743
|
this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
|
|
9118
9744
|
// Shift paragraph formatting with the complete content
|
|
@@ -9360,6 +9986,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9360
9986
|
this.substitutionFields.clear();
|
|
9361
9987
|
this.embeddedObjects.clear();
|
|
9362
9988
|
this.repeatingSections.clear();
|
|
9989
|
+
this.conditionalSections.clear();
|
|
9363
9990
|
this.hyperlinks.clear();
|
|
9364
9991
|
}
|
|
9365
9992
|
// ============================================
|
|
@@ -9716,44 +10343,60 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9716
10343
|
// List Operations
|
|
9717
10344
|
// ============================================
|
|
9718
10345
|
/**
|
|
9719
|
-
*
|
|
10346
|
+
* Get paragraph starts affected by the current selection or cursor position.
|
|
9720
10347
|
*/
|
|
9721
|
-
|
|
10348
|
+
getAffectedParagraphStarts() {
|
|
10349
|
+
const content = this.textState.getText();
|
|
10350
|
+
const selection = this.getSelection();
|
|
10351
|
+
if (selection && selection.start !== selection.end) {
|
|
10352
|
+
return this.paragraphFormatting.getParagraphBoundariesInRange(selection.start, selection.end, content);
|
|
10353
|
+
}
|
|
9722
10354
|
const cursorPos = this.textState.getCursorPosition();
|
|
10355
|
+
return [this.paragraphFormatting.getParagraphStart(cursorPos, content)];
|
|
10356
|
+
}
|
|
10357
|
+
/**
|
|
10358
|
+
* Toggle bullet list for the current paragraph(s) in selection.
|
|
10359
|
+
*/
|
|
10360
|
+
toggleBulletList() {
|
|
9723
10361
|
const content = this.textState.getText();
|
|
9724
|
-
const
|
|
9725
|
-
|
|
9726
|
-
|
|
10362
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10363
|
+
for (const start of paragraphStarts) {
|
|
10364
|
+
this.paragraphFormatting.toggleList(start, 'bullet');
|
|
10365
|
+
}
|
|
10366
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9727
10367
|
}
|
|
9728
10368
|
/**
|
|
9729
|
-
* Toggle numbered list for the current paragraph
|
|
10369
|
+
* Toggle numbered list for the current paragraph(s) in selection.
|
|
9730
10370
|
*/
|
|
9731
10371
|
toggleNumberedList() {
|
|
9732
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9733
10372
|
const content = this.textState.getText();
|
|
9734
|
-
const
|
|
9735
|
-
|
|
9736
|
-
|
|
10373
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10374
|
+
for (const start of paragraphStarts) {
|
|
10375
|
+
this.paragraphFormatting.toggleList(start, 'number');
|
|
10376
|
+
}
|
|
10377
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9737
10378
|
}
|
|
9738
10379
|
/**
|
|
9739
|
-
* Indent the current paragraph
|
|
10380
|
+
* Indent the current paragraph(s) in selection.
|
|
9740
10381
|
*/
|
|
9741
10382
|
indentParagraph() {
|
|
9742
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9743
10383
|
const content = this.textState.getText();
|
|
9744
|
-
const
|
|
9745
|
-
|
|
9746
|
-
|
|
10384
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10385
|
+
for (const start of paragraphStarts) {
|
|
10386
|
+
this.paragraphFormatting.indentParagraph(start);
|
|
10387
|
+
}
|
|
10388
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9747
10389
|
}
|
|
9748
10390
|
/**
|
|
9749
|
-
* Outdent the current paragraph
|
|
10391
|
+
* Outdent the current paragraph(s) in selection.
|
|
9750
10392
|
*/
|
|
9751
10393
|
outdentParagraph() {
|
|
9752
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9753
10394
|
const content = this.textState.getText();
|
|
9754
|
-
const
|
|
9755
|
-
|
|
9756
|
-
|
|
10395
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10396
|
+
for (const start of paragraphStarts) {
|
|
10397
|
+
this.paragraphFormatting.outdentParagraph(start);
|
|
10398
|
+
}
|
|
10399
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9757
10400
|
}
|
|
9758
10401
|
/**
|
|
9759
10402
|
* Get the list formatting for the current paragraph.
|
|
@@ -9915,6 +10558,79 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9915
10558
|
return result;
|
|
9916
10559
|
}
|
|
9917
10560
|
// ============================================
|
|
10561
|
+
// Conditional Section Operations
|
|
10562
|
+
// ============================================
|
|
10563
|
+
/**
|
|
10564
|
+
* Get the conditional section manager.
|
|
10565
|
+
*/
|
|
10566
|
+
getConditionalSectionManager() {
|
|
10567
|
+
return this.conditionalSections;
|
|
10568
|
+
}
|
|
10569
|
+
/**
|
|
10570
|
+
* Get all conditional sections.
|
|
10571
|
+
*/
|
|
10572
|
+
getConditionalSections() {
|
|
10573
|
+
return this.conditionalSections.getSections();
|
|
10574
|
+
}
|
|
10575
|
+
/**
|
|
10576
|
+
* Create a conditional section.
|
|
10577
|
+
* @param startIndex Text index at paragraph start (must be at a paragraph boundary)
|
|
10578
|
+
* @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
|
|
10579
|
+
* @param predicate The predicate expression to evaluate
|
|
10580
|
+
* @returns The created section, or null if boundaries are invalid
|
|
10581
|
+
*/
|
|
10582
|
+
createConditionalSection(startIndex, endIndex, predicate) {
|
|
10583
|
+
const content = this.textState.getText();
|
|
10584
|
+
if (!this.conditionalSections.validateBoundaries(startIndex, endIndex, content)) {
|
|
10585
|
+
return null;
|
|
10586
|
+
}
|
|
10587
|
+
const section = this.conditionalSections.create(startIndex, endIndex, predicate);
|
|
10588
|
+
this.emit('content-changed', {
|
|
10589
|
+
text: content,
|
|
10590
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10591
|
+
});
|
|
10592
|
+
return section;
|
|
10593
|
+
}
|
|
10594
|
+
/**
|
|
10595
|
+
* Remove a conditional section by ID.
|
|
10596
|
+
*/
|
|
10597
|
+
removeConditionalSection(id) {
|
|
10598
|
+
const section = this.conditionalSections.remove(id);
|
|
10599
|
+
if (section) {
|
|
10600
|
+
this.emit('content-changed', {
|
|
10601
|
+
text: this.textState.getText(),
|
|
10602
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10603
|
+
});
|
|
10604
|
+
return true;
|
|
10605
|
+
}
|
|
10606
|
+
return false;
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* Get a conditional section by ID.
|
|
10610
|
+
*/
|
|
10611
|
+
getConditionalSection(id) {
|
|
10612
|
+
return this.conditionalSections.getSection(id);
|
|
10613
|
+
}
|
|
10614
|
+
/**
|
|
10615
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
10616
|
+
*/
|
|
10617
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
10618
|
+
return this.conditionalSections.getSectionAtBoundary(textIndex);
|
|
10619
|
+
}
|
|
10620
|
+
/**
|
|
10621
|
+
* Update a conditional section's predicate.
|
|
10622
|
+
*/
|
|
10623
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
10624
|
+
const result = this.conditionalSections.updatePredicate(id, predicate);
|
|
10625
|
+
if (result) {
|
|
10626
|
+
this.emit('content-changed', {
|
|
10627
|
+
text: this.textState.getText(),
|
|
10628
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10629
|
+
});
|
|
10630
|
+
}
|
|
10631
|
+
return result;
|
|
10632
|
+
}
|
|
10633
|
+
// ============================================
|
|
9918
10634
|
// Serialization
|
|
9919
10635
|
// ============================================
|
|
9920
10636
|
/**
|
|
@@ -9968,6 +10684,8 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9968
10684
|
}));
|
|
9969
10685
|
// Serialize repeating sections
|
|
9970
10686
|
const repeatingSectionsData = this.repeatingSections.toJSON();
|
|
10687
|
+
// Serialize conditional sections
|
|
10688
|
+
const conditionalSectionsData = this.conditionalSections.toJSON();
|
|
9971
10689
|
// Serialize embedded objects
|
|
9972
10690
|
const embeddedObjects = [];
|
|
9973
10691
|
const objectsMap = this.embeddedObjects.getObjects();
|
|
@@ -9985,6 +10703,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9985
10703
|
paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
|
|
9986
10704
|
substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
|
|
9987
10705
|
repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
|
|
10706
|
+
conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
|
|
9988
10707
|
embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
|
|
9989
10708
|
hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
|
|
9990
10709
|
};
|
|
@@ -10023,6 +10742,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10023
10742
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10024
10743
|
content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
|
|
10025
10744
|
}
|
|
10745
|
+
// Restore conditional sections
|
|
10746
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10747
|
+
content.getConditionalSectionManager().fromJSON(data.conditionalSections);
|
|
10748
|
+
}
|
|
10026
10749
|
// Restore embedded objects using factory
|
|
10027
10750
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10028
10751
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10077,6 +10800,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10077
10800
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10078
10801
|
this.repeatingSections.fromJSON(data.repeatingSections);
|
|
10079
10802
|
}
|
|
10803
|
+
// Restore conditional sections
|
|
10804
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10805
|
+
this.conditionalSections.fromJSON(data.conditionalSections);
|
|
10806
|
+
}
|
|
10080
10807
|
// Restore embedded objects
|
|
10081
10808
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10082
10809
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10096,6 +10823,349 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10096
10823
|
}
|
|
10097
10824
|
}
|
|
10098
10825
|
|
|
10826
|
+
/**
|
|
10827
|
+
* Simple recursive-descent predicate evaluator.
|
|
10828
|
+
* Supports:
|
|
10829
|
+
* - Truthiness: `isActive`
|
|
10830
|
+
* - Negation: `!isActive`
|
|
10831
|
+
* - Comparisons: ==, !=, >, <, >=, <=
|
|
10832
|
+
* - Logical: &&, ||, parentheses
|
|
10833
|
+
* - Literals: "approved", 100, true/false
|
|
10834
|
+
* - Dot notation: customer.isVIP
|
|
10835
|
+
*/
|
|
10836
|
+
function tokenize(input) {
|
|
10837
|
+
const tokens = [];
|
|
10838
|
+
let i = 0;
|
|
10839
|
+
while (i < input.length) {
|
|
10840
|
+
const ch = input[i];
|
|
10841
|
+
// Skip whitespace
|
|
10842
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
10843
|
+
i++;
|
|
10844
|
+
continue;
|
|
10845
|
+
}
|
|
10846
|
+
// Parentheses
|
|
10847
|
+
if (ch === '(' || ch === ')') {
|
|
10848
|
+
tokens.push({ type: 'paren', value: ch });
|
|
10849
|
+
i++;
|
|
10850
|
+
continue;
|
|
10851
|
+
}
|
|
10852
|
+
// Two-character operators
|
|
10853
|
+
if (i + 1 < input.length) {
|
|
10854
|
+
const two = input[i] + input[i + 1];
|
|
10855
|
+
if (two === '==' || two === '!=' || two === '>=' || two === '<=' || two === '&&' || two === '||' || two === '=~' || two === '!~') {
|
|
10856
|
+
tokens.push({ type: 'operator', value: two });
|
|
10857
|
+
i += 2;
|
|
10858
|
+
continue;
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
// Single-character operators
|
|
10862
|
+
if (ch === '>' || ch === '<') {
|
|
10863
|
+
tokens.push({ type: 'operator', value: ch });
|
|
10864
|
+
i++;
|
|
10865
|
+
continue;
|
|
10866
|
+
}
|
|
10867
|
+
// Not operator
|
|
10868
|
+
if (ch === '!') {
|
|
10869
|
+
tokens.push({ type: 'not' });
|
|
10870
|
+
i++;
|
|
10871
|
+
continue;
|
|
10872
|
+
}
|
|
10873
|
+
// String literals
|
|
10874
|
+
if (ch === '"' || ch === "'") {
|
|
10875
|
+
const quote = ch;
|
|
10876
|
+
i++;
|
|
10877
|
+
let str = '';
|
|
10878
|
+
while (i < input.length && input[i] !== quote) {
|
|
10879
|
+
if (input[i] === '\\' && i + 1 < input.length) {
|
|
10880
|
+
i++;
|
|
10881
|
+
str += input[i];
|
|
10882
|
+
}
|
|
10883
|
+
else {
|
|
10884
|
+
str += input[i];
|
|
10885
|
+
}
|
|
10886
|
+
i++;
|
|
10887
|
+
}
|
|
10888
|
+
i++; // skip closing quote
|
|
10889
|
+
tokens.push({ type: 'string', value: str });
|
|
10890
|
+
continue;
|
|
10891
|
+
}
|
|
10892
|
+
// Numbers
|
|
10893
|
+
if (ch >= '0' && ch <= '9') {
|
|
10894
|
+
let num = '';
|
|
10895
|
+
while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
|
|
10896
|
+
num += input[i];
|
|
10897
|
+
i++;
|
|
10898
|
+
}
|
|
10899
|
+
tokens.push({ type: 'number', value: parseFloat(num) });
|
|
10900
|
+
continue;
|
|
10901
|
+
}
|
|
10902
|
+
// Identifiers (including dot notation: customer.isVIP)
|
|
10903
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
10904
|
+
let ident = '';
|
|
10905
|
+
while (i < input.length &&
|
|
10906
|
+
((input[i] >= 'a' && input[i] <= 'z') ||
|
|
10907
|
+
(input[i] >= 'A' && input[i] <= 'Z') ||
|
|
10908
|
+
(input[i] >= '0' && input[i] <= '9') ||
|
|
10909
|
+
input[i] === '_' || input[i] === '$' || input[i] === '.')) {
|
|
10910
|
+
ident += input[i];
|
|
10911
|
+
i++;
|
|
10912
|
+
}
|
|
10913
|
+
if (ident === 'true') {
|
|
10914
|
+
tokens.push({ type: 'boolean', value: true });
|
|
10915
|
+
}
|
|
10916
|
+
else if (ident === 'false') {
|
|
10917
|
+
tokens.push({ type: 'boolean', value: false });
|
|
10918
|
+
}
|
|
10919
|
+
else {
|
|
10920
|
+
tokens.push({ type: 'identifier', value: ident });
|
|
10921
|
+
}
|
|
10922
|
+
continue;
|
|
10923
|
+
}
|
|
10924
|
+
// Unknown character — skip
|
|
10925
|
+
i++;
|
|
10926
|
+
}
|
|
10927
|
+
tokens.push({ type: 'eof' });
|
|
10928
|
+
return tokens;
|
|
10929
|
+
}
|
|
10930
|
+
class Parser {
|
|
10931
|
+
constructor(tokens, data) {
|
|
10932
|
+
this.pos = 0;
|
|
10933
|
+
this.tokens = tokens;
|
|
10934
|
+
this.data = data;
|
|
10935
|
+
}
|
|
10936
|
+
peek() {
|
|
10937
|
+
return this.tokens[this.pos];
|
|
10938
|
+
}
|
|
10939
|
+
advance() {
|
|
10940
|
+
const token = this.tokens[this.pos];
|
|
10941
|
+
this.pos++;
|
|
10942
|
+
return token;
|
|
10943
|
+
}
|
|
10944
|
+
/**
|
|
10945
|
+
* Parse the full expression.
|
|
10946
|
+
* Grammar:
|
|
10947
|
+
* expr → or_expr
|
|
10948
|
+
* or_expr → and_expr ('||' and_expr)*
|
|
10949
|
+
* and_expr → unary (('==' | '!=' | '>' | '<' | '>=' | '<=') unary)?
|
|
10950
|
+
* | unary ('&&' unary_or_comparison)*
|
|
10951
|
+
* unary → '!' unary | primary
|
|
10952
|
+
* primary → '(' expr ')' | literal | identifier
|
|
10953
|
+
*/
|
|
10954
|
+
parse() {
|
|
10955
|
+
const result = this.parseOr();
|
|
10956
|
+
return result;
|
|
10957
|
+
}
|
|
10958
|
+
parseOr() {
|
|
10959
|
+
let left = this.parseAnd();
|
|
10960
|
+
while (this.peek().type === 'operator' && this.peek().value === '||') {
|
|
10961
|
+
this.advance();
|
|
10962
|
+
const right = this.parseAnd();
|
|
10963
|
+
left = this.isTruthy(left) || this.isTruthy(right);
|
|
10964
|
+
}
|
|
10965
|
+
return left;
|
|
10966
|
+
}
|
|
10967
|
+
parseAnd() {
|
|
10968
|
+
let left = this.parseComparison();
|
|
10969
|
+
while (this.peek().type === 'operator' && this.peek().value === '&&') {
|
|
10970
|
+
this.advance();
|
|
10971
|
+
const right = this.parseComparison();
|
|
10972
|
+
left = this.isTruthy(left) && this.isTruthy(right);
|
|
10973
|
+
}
|
|
10974
|
+
return left;
|
|
10975
|
+
}
|
|
10976
|
+
parseComparison() {
|
|
10977
|
+
const left = this.parseUnary();
|
|
10978
|
+
const token = this.peek();
|
|
10979
|
+
if (token.type === 'operator') {
|
|
10980
|
+
const op = token.value;
|
|
10981
|
+
if (op === '==' || op === '!=' || op === '>' || op === '<' || op === '>=' || op === '<=' || op === '=~' || op === '!~') {
|
|
10982
|
+
this.advance();
|
|
10983
|
+
const right = this.parseUnary();
|
|
10984
|
+
return this.compare(left, op, right);
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
return left;
|
|
10988
|
+
}
|
|
10989
|
+
parseUnary() {
|
|
10990
|
+
if (this.peek().type === 'not') {
|
|
10991
|
+
this.advance();
|
|
10992
|
+
const value = this.parseUnary();
|
|
10993
|
+
return !this.isTruthy(value);
|
|
10994
|
+
}
|
|
10995
|
+
return this.parsePrimary();
|
|
10996
|
+
}
|
|
10997
|
+
parsePrimary() {
|
|
10998
|
+
const token = this.peek();
|
|
10999
|
+
if (token.type === 'paren' && token.value === '(') {
|
|
11000
|
+
this.advance();
|
|
11001
|
+
const value = this.parseOr();
|
|
11002
|
+
// Consume closing paren
|
|
11003
|
+
if (this.peek().type === 'paren' && this.peek().value === ')') {
|
|
11004
|
+
this.advance();
|
|
11005
|
+
}
|
|
11006
|
+
return value;
|
|
11007
|
+
}
|
|
11008
|
+
if (token.type === 'string') {
|
|
11009
|
+
this.advance();
|
|
11010
|
+
return token.value;
|
|
11011
|
+
}
|
|
11012
|
+
if (token.type === 'number') {
|
|
11013
|
+
this.advance();
|
|
11014
|
+
return token.value;
|
|
11015
|
+
}
|
|
11016
|
+
if (token.type === 'boolean') {
|
|
11017
|
+
this.advance();
|
|
11018
|
+
return token.value;
|
|
11019
|
+
}
|
|
11020
|
+
if (token.type === 'identifier') {
|
|
11021
|
+
this.advance();
|
|
11022
|
+
return this.resolveIdentifier(token.value);
|
|
11023
|
+
}
|
|
11024
|
+
// EOF or unexpected — return undefined
|
|
11025
|
+
this.advance();
|
|
11026
|
+
return undefined;
|
|
11027
|
+
}
|
|
11028
|
+
resolveIdentifier(path) {
|
|
11029
|
+
const parts = path.split('.');
|
|
11030
|
+
let current = this.data;
|
|
11031
|
+
for (const part of parts) {
|
|
11032
|
+
if (current === null || current === undefined) {
|
|
11033
|
+
return undefined;
|
|
11034
|
+
}
|
|
11035
|
+
if (typeof current === 'object') {
|
|
11036
|
+
current = current[part];
|
|
11037
|
+
}
|
|
11038
|
+
else {
|
|
11039
|
+
return undefined;
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
return current;
|
|
11043
|
+
}
|
|
11044
|
+
compare(left, op, right) {
|
|
11045
|
+
// Regex match: left is coerced to string, right is the pattern string
|
|
11046
|
+
if (op === '=~' || op === '!~') {
|
|
11047
|
+
const str = this.toString(left);
|
|
11048
|
+
const pattern = this.toString(right);
|
|
11049
|
+
try {
|
|
11050
|
+
const regex = new RegExp(pattern);
|
|
11051
|
+
const matches = regex.test(str);
|
|
11052
|
+
return op === '=~' ? matches : !matches;
|
|
11053
|
+
}
|
|
11054
|
+
catch {
|
|
11055
|
+
// Invalid regex pattern — treat as no match
|
|
11056
|
+
return op === '!~';
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
// For ordering operators, coerce both sides to numbers if either side is numeric
|
|
11060
|
+
if (op === '>' || op === '<' || op === '>=' || op === '<=') {
|
|
11061
|
+
const l = this.toNumber(left);
|
|
11062
|
+
const r = this.toNumber(right);
|
|
11063
|
+
switch (op) {
|
|
11064
|
+
case '>': return l > r;
|
|
11065
|
+
case '<': return l < r;
|
|
11066
|
+
case '>=': return l >= r;
|
|
11067
|
+
case '<=': return l <= r;
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
// For equality, coerce to numbers if both sides look numeric
|
|
11071
|
+
const ln = this.toNumberIfNumeric(left);
|
|
11072
|
+
const rn = this.toNumberIfNumeric(right);
|
|
11073
|
+
switch (op) {
|
|
11074
|
+
case '==': return ln == rn; // eslint-disable-line eqeqeq
|
|
11075
|
+
case '!=': return ln != rn; // eslint-disable-line eqeqeq
|
|
11076
|
+
default: return false;
|
|
11077
|
+
}
|
|
11078
|
+
}
|
|
11079
|
+
/**
|
|
11080
|
+
* Convert a value to a string for regex matching.
|
|
11081
|
+
*/
|
|
11082
|
+
toString(value) {
|
|
11083
|
+
if (value === null || value === undefined)
|
|
11084
|
+
return '';
|
|
11085
|
+
if (typeof value === 'string')
|
|
11086
|
+
return value;
|
|
11087
|
+
return String(value);
|
|
11088
|
+
}
|
|
11089
|
+
/**
|
|
11090
|
+
* Convert a value to a number. Strings that look like numbers are parsed.
|
|
11091
|
+
* Non-numeric values become NaN.
|
|
11092
|
+
*/
|
|
11093
|
+
toNumber(value) {
|
|
11094
|
+
if (typeof value === 'number')
|
|
11095
|
+
return value;
|
|
11096
|
+
if (typeof value === 'string') {
|
|
11097
|
+
const n = Number(value);
|
|
11098
|
+
return isNaN(n) ? NaN : n;
|
|
11099
|
+
}
|
|
11100
|
+
if (typeof value === 'boolean')
|
|
11101
|
+
return value ? 1 : 0;
|
|
11102
|
+
return NaN;
|
|
11103
|
+
}
|
|
11104
|
+
/**
|
|
11105
|
+
* If a value is a string that looks like a number, convert it.
|
|
11106
|
+
* Otherwise return the value as-is. Used for == / != so that
|
|
11107
|
+
* "5" == 5 is true but "hello" == "hello" still works.
|
|
11108
|
+
*/
|
|
11109
|
+
toNumberIfNumeric(value) {
|
|
11110
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
11111
|
+
const n = Number(value);
|
|
11112
|
+
if (!isNaN(n))
|
|
11113
|
+
return n;
|
|
11114
|
+
}
|
|
11115
|
+
return value;
|
|
11116
|
+
}
|
|
11117
|
+
isTruthy(value) {
|
|
11118
|
+
if (value === null || value === undefined)
|
|
11119
|
+
return false;
|
|
11120
|
+
if (typeof value === 'boolean')
|
|
11121
|
+
return value;
|
|
11122
|
+
if (typeof value === 'number')
|
|
11123
|
+
return value !== 0;
|
|
11124
|
+
if (typeof value === 'string')
|
|
11125
|
+
return value.length > 0;
|
|
11126
|
+
if (Array.isArray(value))
|
|
11127
|
+
return value.length > 0;
|
|
11128
|
+
return true;
|
|
11129
|
+
}
|
|
11130
|
+
}
|
|
11131
|
+
/**
|
|
11132
|
+
* Static predicate evaluator for conditional sections.
|
|
11133
|
+
*/
|
|
11134
|
+
class PredicateEvaluator {
|
|
11135
|
+
/**
|
|
11136
|
+
* Evaluate a predicate expression against data.
|
|
11137
|
+
* @param predicate The predicate string (e.g., "isActive", "count > 0")
|
|
11138
|
+
* @param data The data context to evaluate against
|
|
11139
|
+
* @returns true if the predicate is truthy, false otherwise
|
|
11140
|
+
*/
|
|
11141
|
+
static evaluate(predicate, data) {
|
|
11142
|
+
if (!predicate || predicate.trim().length === 0) {
|
|
11143
|
+
return false;
|
|
11144
|
+
}
|
|
11145
|
+
try {
|
|
11146
|
+
const tokens = tokenize(predicate.trim());
|
|
11147
|
+
const parser = new Parser(tokens, data);
|
|
11148
|
+
const result = parser.parse();
|
|
11149
|
+
// Convert result to boolean
|
|
11150
|
+
if (result === null || result === undefined)
|
|
11151
|
+
return false;
|
|
11152
|
+
if (typeof result === 'boolean')
|
|
11153
|
+
return result;
|
|
11154
|
+
if (typeof result === 'number')
|
|
11155
|
+
return result !== 0;
|
|
11156
|
+
if (typeof result === 'string')
|
|
11157
|
+
return result.length > 0;
|
|
11158
|
+
if (Array.isArray(result))
|
|
11159
|
+
return result.length > 0;
|
|
11160
|
+
return true;
|
|
11161
|
+
}
|
|
11162
|
+
catch {
|
|
11163
|
+
// If evaluation fails, treat as false
|
|
11164
|
+
return false;
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
|
|
10099
11169
|
/**
|
|
10100
11170
|
* Abstract base class providing common functionality for regions.
|
|
10101
11171
|
*/
|
|
@@ -11131,6 +12201,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
|
|
|
11131
12201
|
const LOOP_LABEL_PADDING = 4;
|
|
11132
12202
|
const LOOP_LABEL_RADIUS = 4;
|
|
11133
12203
|
const LOOP_LINE_DASH = [4, 4];
|
|
12204
|
+
// Conditional section indicator styling
|
|
12205
|
+
const COND_INDICATOR_COLOR = '#D97706'; // Orange
|
|
12206
|
+
const COND_LABEL_PADDING = 4;
|
|
12207
|
+
const COND_LABEL_RADIUS = 4;
|
|
12208
|
+
const COND_LINE_DASH = [4, 4];
|
|
11134
12209
|
// Hyperlink styling
|
|
11135
12210
|
const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
|
|
11136
12211
|
class FlowingTextRenderer extends EventEmitter {
|
|
@@ -11388,8 +12463,6 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11388
12463
|
if (pageIndex === 0) {
|
|
11389
12464
|
// Clear table continuations when starting a new render cycle
|
|
11390
12465
|
this.clearTableContinuations();
|
|
11391
|
-
// Clear content hit targets - they will be re-registered during render
|
|
11392
|
-
this._hitTestManager.clearCategory('content');
|
|
11393
12466
|
// This is the first page, flow all text
|
|
11394
12467
|
const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
|
|
11395
12468
|
this.flowedPages.set(page.id, flowedPages);
|
|
@@ -11689,6 +12762,8 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11689
12762
|
const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
|
|
11690
12763
|
// Get hyperlinks for rendering
|
|
11691
12764
|
const hyperlinks = flowingContent.getAllHyperlinks();
|
|
12765
|
+
// Track relative objects to render after all lines (so they appear on top)
|
|
12766
|
+
const relativeObjects = [];
|
|
11692
12767
|
// Render each line
|
|
11693
12768
|
let y = bounds.y;
|
|
11694
12769
|
for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
|
|
@@ -11701,6 +12776,18 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11701
12776
|
if (clipToBounds && y > bounds.y + bounds.height) {
|
|
11702
12777
|
break;
|
|
11703
12778
|
}
|
|
12779
|
+
// Collect relative objects from this line
|
|
12780
|
+
if (line.embeddedObjects) {
|
|
12781
|
+
for (const embeddedObj of line.embeddedObjects) {
|
|
12782
|
+
if (embeddedObj.isAnchor && embeddedObj.object.position === 'relative') {
|
|
12783
|
+
relativeObjects.push({
|
|
12784
|
+
object: embeddedObj.object,
|
|
12785
|
+
anchorX: bounds.x,
|
|
12786
|
+
anchorY: y
|
|
12787
|
+
});
|
|
12788
|
+
}
|
|
12789
|
+
}
|
|
12790
|
+
}
|
|
11704
12791
|
this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
|
|
11705
12792
|
y += line.height;
|
|
11706
12793
|
}
|
|
@@ -11711,6 +12798,10 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11711
12798
|
if (clipToBounds) {
|
|
11712
12799
|
ctx.restore();
|
|
11713
12800
|
}
|
|
12801
|
+
// Render relative objects on top of text (outside clip region)
|
|
12802
|
+
if (relativeObjects.length > 0) {
|
|
12803
|
+
this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
|
|
12804
|
+
}
|
|
11714
12805
|
}
|
|
11715
12806
|
/**
|
|
11716
12807
|
* Render selection highlight for a region.
|
|
@@ -13866,43 +14957,256 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13866
14957
|
}
|
|
13867
14958
|
y += line.height;
|
|
13868
14959
|
}
|
|
13869
|
-
// Check if text index is just past the last line (end of content)
|
|
13870
|
-
if (flowedPage.lines.length > 0) {
|
|
13871
|
-
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
13872
|
-
if (textIndex === lastLine.endIndex + 1) {
|
|
13873
|
-
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
13874
|
-
}
|
|
14960
|
+
// Check if text index is just past the last line (end of content)
|
|
14961
|
+
if (flowedPage.lines.length > 0) {
|
|
14962
|
+
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
14963
|
+
if (textIndex === lastLine.endIndex + 1) {
|
|
14964
|
+
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14967
|
+
return null;
|
|
14968
|
+
}
|
|
14969
|
+
/**
|
|
14970
|
+
* Check if a section spans across a flowed page (starts before and ends after).
|
|
14971
|
+
*/
|
|
14972
|
+
sectionSpansPage(section, flowedPage) {
|
|
14973
|
+
if (flowedPage.lines.length === 0)
|
|
14974
|
+
return false;
|
|
14975
|
+
const pageStart = flowedPage.startIndex;
|
|
14976
|
+
const pageEnd = flowedPage.endIndex;
|
|
14977
|
+
// Section spans this page if it started before and ends after
|
|
14978
|
+
return section.startIndex < pageStart && section.endIndex > pageEnd;
|
|
14979
|
+
}
|
|
14980
|
+
/**
|
|
14981
|
+
* Get a repeating section at a point (for click detection).
|
|
14982
|
+
* Checks if the point is on the Loop label or vertical connector.
|
|
14983
|
+
*/
|
|
14984
|
+
getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
|
|
14985
|
+
const labelX = pageBounds.x + 5;
|
|
14986
|
+
const labelWidth = 32;
|
|
14987
|
+
const connectorX = labelX + labelWidth / 2;
|
|
14988
|
+
const hitRadius = 10; // Pixels for click detection
|
|
14989
|
+
for (const section of sections) {
|
|
14990
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
14991
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
14992
|
+
const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
|
|
14993
|
+
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
14994
|
+
continue;
|
|
14995
|
+
}
|
|
14996
|
+
// Check if click is on the Loop label
|
|
14997
|
+
if (startInfo) {
|
|
14998
|
+
const labelY = startInfo.y - 10;
|
|
14999
|
+
const labelHeight = 18;
|
|
15000
|
+
if (point.x >= labelX &&
|
|
15001
|
+
point.x <= labelX + labelWidth &&
|
|
15002
|
+
point.y >= labelY &&
|
|
15003
|
+
point.y <= labelY + labelHeight) {
|
|
15004
|
+
return section;
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
15007
|
+
// Check if click is on the vertical connector line
|
|
15008
|
+
let verticalStartY;
|
|
15009
|
+
let verticalEndY;
|
|
15010
|
+
if (startInfo) {
|
|
15011
|
+
verticalStartY = startInfo.y;
|
|
15012
|
+
}
|
|
15013
|
+
else {
|
|
15014
|
+
verticalStartY = contentBounds.y;
|
|
15015
|
+
}
|
|
15016
|
+
if (endInfo) {
|
|
15017
|
+
verticalEndY = endInfo.y;
|
|
15018
|
+
}
|
|
15019
|
+
else if (sectionSpansThisPage) {
|
|
15020
|
+
verticalEndY = contentBounds.y + flowedPage.height;
|
|
15021
|
+
}
|
|
15022
|
+
else {
|
|
15023
|
+
continue;
|
|
15024
|
+
}
|
|
15025
|
+
if (Math.abs(point.x - connectorX) <= hitRadius &&
|
|
15026
|
+
point.y >= verticalStartY &&
|
|
15027
|
+
point.y <= verticalEndY) {
|
|
15028
|
+
return section;
|
|
15029
|
+
}
|
|
15030
|
+
}
|
|
15031
|
+
return null;
|
|
15032
|
+
}
|
|
15033
|
+
// ============================================
|
|
15034
|
+
// Conditional Section Indicators
|
|
15035
|
+
// ============================================
|
|
15036
|
+
/**
|
|
15037
|
+
* Render conditional section indicators for a page.
|
|
15038
|
+
*/
|
|
15039
|
+
renderConditionalSectionIndicators(sections, pageIndex, ctx, contentBounds, flowedPage, pageBounds, selectedSectionId = null) {
|
|
15040
|
+
for (const section of sections) {
|
|
15041
|
+
this.renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, pageBounds, section.id === selectedSectionId);
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
/**
|
|
15045
|
+
* Render a single conditional section indicator.
|
|
15046
|
+
*/
|
|
15047
|
+
renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, _pageBounds, isSelected = false) {
|
|
15048
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
15049
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
15050
|
+
const sectionOverlapsPage = section.startIndex < flowedPage.endIndex &&
|
|
15051
|
+
section.endIndex > flowedPage.startIndex;
|
|
15052
|
+
if (!sectionOverlapsPage) {
|
|
15053
|
+
return;
|
|
15054
|
+
}
|
|
15055
|
+
const hasStart = startInfo !== null;
|
|
15056
|
+
const hasEnd = endInfo !== null;
|
|
15057
|
+
const startsBeforePage = section.startIndex < flowedPage.startIndex;
|
|
15058
|
+
const endsAfterPage = section.endIndex > flowedPage.endIndex;
|
|
15059
|
+
ctx.save();
|
|
15060
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
15061
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15062
|
+
ctx.lineWidth = 1;
|
|
15063
|
+
// Position on the right side of the content area
|
|
15064
|
+
const labelWidth = 22;
|
|
15065
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
15066
|
+
const connectorX = labelX + labelWidth / 2;
|
|
15067
|
+
// Draw start indicator lines
|
|
15068
|
+
if (hasStart) {
|
|
15069
|
+
const startY = startInfo.y;
|
|
15070
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15071
|
+
ctx.beginPath();
|
|
15072
|
+
ctx.moveTo(contentBounds.x, startY);
|
|
15073
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, startY);
|
|
15074
|
+
ctx.stroke();
|
|
15075
|
+
ctx.setLineDash([]);
|
|
15076
|
+
ctx.beginPath();
|
|
15077
|
+
ctx.moveTo(contentBounds.x + contentBounds.width, startY);
|
|
15078
|
+
ctx.lineTo(labelX, startY);
|
|
15079
|
+
ctx.stroke();
|
|
15080
|
+
}
|
|
15081
|
+
else if (startsBeforePage) {
|
|
15082
|
+
const topY = contentBounds.y;
|
|
15083
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15084
|
+
ctx.beginPath();
|
|
15085
|
+
ctx.moveTo(contentBounds.x, topY);
|
|
15086
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15087
|
+
ctx.stroke();
|
|
15088
|
+
ctx.setLineDash([]);
|
|
15089
|
+
ctx.beginPath();
|
|
15090
|
+
ctx.moveTo(connectorX, topY);
|
|
15091
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15092
|
+
ctx.stroke();
|
|
15093
|
+
}
|
|
15094
|
+
// Draw end indicator
|
|
15095
|
+
if (hasEnd) {
|
|
15096
|
+
const endY = endInfo.y;
|
|
15097
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15098
|
+
ctx.beginPath();
|
|
15099
|
+
ctx.moveTo(contentBounds.x, endY);
|
|
15100
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15101
|
+
ctx.stroke();
|
|
15102
|
+
ctx.setLineDash([]);
|
|
15103
|
+
ctx.beginPath();
|
|
15104
|
+
ctx.moveTo(connectorX, endY);
|
|
15105
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15106
|
+
ctx.stroke();
|
|
15107
|
+
}
|
|
15108
|
+
else if (endsAfterPage) {
|
|
15109
|
+
const bottomY = contentBounds.y + contentBounds.height;
|
|
15110
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15111
|
+
ctx.beginPath();
|
|
15112
|
+
ctx.moveTo(contentBounds.x, bottomY);
|
|
15113
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15114
|
+
ctx.stroke();
|
|
15115
|
+
ctx.setLineDash([]);
|
|
15116
|
+
ctx.beginPath();
|
|
15117
|
+
ctx.moveTo(connectorX, bottomY);
|
|
15118
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15119
|
+
ctx.stroke();
|
|
15120
|
+
}
|
|
15121
|
+
// Draw vertical connector line
|
|
15122
|
+
let verticalStartY;
|
|
15123
|
+
let verticalEndY;
|
|
15124
|
+
if (hasStart) {
|
|
15125
|
+
verticalStartY = startInfo.y;
|
|
15126
|
+
}
|
|
15127
|
+
else if (startsBeforePage) {
|
|
15128
|
+
verticalStartY = contentBounds.y;
|
|
15129
|
+
}
|
|
15130
|
+
else {
|
|
15131
|
+
verticalStartY = contentBounds.y;
|
|
15132
|
+
}
|
|
15133
|
+
if (hasEnd) {
|
|
15134
|
+
verticalEndY = endInfo.y;
|
|
15135
|
+
}
|
|
15136
|
+
else if (endsAfterPage) {
|
|
15137
|
+
verticalEndY = contentBounds.y + contentBounds.height;
|
|
15138
|
+
}
|
|
15139
|
+
else {
|
|
15140
|
+
verticalEndY = verticalStartY;
|
|
13875
15141
|
}
|
|
13876
|
-
|
|
15142
|
+
if (verticalEndY > verticalStartY) {
|
|
15143
|
+
ctx.beginPath();
|
|
15144
|
+
ctx.moveTo(connectorX, verticalStartY);
|
|
15145
|
+
ctx.lineTo(connectorX, verticalEndY);
|
|
15146
|
+
ctx.stroke();
|
|
15147
|
+
}
|
|
15148
|
+
// Draw "If" label
|
|
15149
|
+
if (hasStart) {
|
|
15150
|
+
const startY = startInfo.y;
|
|
15151
|
+
this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
|
|
15152
|
+
}
|
|
15153
|
+
// Update visual state
|
|
15154
|
+
section.visualState = {
|
|
15155
|
+
startPageIndex: hasStart ? pageIndex : -1,
|
|
15156
|
+
startY: hasStart ? startInfo.y : 0,
|
|
15157
|
+
endPageIndex: hasEnd ? pageIndex : -1,
|
|
15158
|
+
endY: hasEnd ? endInfo.y : 0,
|
|
15159
|
+
spansMultiplePages: !hasStart || !hasEnd
|
|
15160
|
+
};
|
|
15161
|
+
ctx.restore();
|
|
13877
15162
|
}
|
|
13878
15163
|
/**
|
|
13879
|
-
*
|
|
15164
|
+
* Draw the "If" label in a rounded rectangle.
|
|
13880
15165
|
*/
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
const
|
|
13885
|
-
const
|
|
13886
|
-
|
|
13887
|
-
|
|
15166
|
+
drawCondLabel(ctx, x, y, text, isSelected = false) {
|
|
15167
|
+
ctx.save();
|
|
15168
|
+
ctx.font = '10px Arial';
|
|
15169
|
+
const metrics = ctx.measureText(text);
|
|
15170
|
+
const textWidth = metrics.width;
|
|
15171
|
+
const textHeight = 10;
|
|
15172
|
+
const boxWidth = textWidth + COND_LABEL_PADDING * 2;
|
|
15173
|
+
const boxHeight = textHeight + COND_LABEL_PADDING * 2;
|
|
15174
|
+
ctx.beginPath();
|
|
15175
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
|
|
15176
|
+
if (isSelected) {
|
|
15177
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15178
|
+
ctx.fill();
|
|
15179
|
+
ctx.fillStyle = '#ffffff';
|
|
15180
|
+
}
|
|
15181
|
+
else {
|
|
15182
|
+
ctx.fillStyle = '#ffffff';
|
|
15183
|
+
ctx.fill();
|
|
15184
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
15185
|
+
ctx.lineWidth = 1.5;
|
|
15186
|
+
ctx.stroke();
|
|
15187
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15188
|
+
}
|
|
15189
|
+
ctx.textBaseline = 'middle';
|
|
15190
|
+
ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
|
|
15191
|
+
ctx.restore();
|
|
13888
15192
|
}
|
|
13889
15193
|
/**
|
|
13890
|
-
* Get a
|
|
13891
|
-
* Checks if the point is on the Loop label or vertical connector.
|
|
15194
|
+
* Get a conditional section at a point (for click detection).
|
|
13892
15195
|
*/
|
|
13893
|
-
|
|
13894
|
-
const
|
|
13895
|
-
const
|
|
15196
|
+
getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
|
|
15197
|
+
const labelWidth = 22;
|
|
15198
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
13896
15199
|
const connectorX = labelX + labelWidth / 2;
|
|
13897
|
-
const hitRadius = 10;
|
|
15200
|
+
const hitRadius = 10;
|
|
13898
15201
|
for (const section of sections) {
|
|
13899
15202
|
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
13900
15203
|
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
13901
|
-
const sectionSpansThisPage =
|
|
15204
|
+
const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
|
|
15205
|
+
section.endIndex > flowedPage.endIndex;
|
|
13902
15206
|
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
13903
15207
|
continue;
|
|
13904
15208
|
}
|
|
13905
|
-
// Check if click is on the
|
|
15209
|
+
// Check if click is on the "If" label
|
|
13906
15210
|
if (startInfo) {
|
|
13907
15211
|
const labelY = startInfo.y - 10;
|
|
13908
15212
|
const labelHeight = 18;
|
|
@@ -13967,6 +15271,7 @@ class CanvasManager extends EventEmitter {
|
|
|
13967
15271
|
this.isSelectingText = false;
|
|
13968
15272
|
this.textSelectionStartPageId = null;
|
|
13969
15273
|
this.selectedSectionId = null;
|
|
15274
|
+
this.selectedConditionalSectionId = null;
|
|
13970
15275
|
this._activeSection = 'body';
|
|
13971
15276
|
this.lastClickTime = 0;
|
|
13972
15277
|
this.lastClickPosition = null;
|
|
@@ -14106,6 +15411,11 @@ class CanvasManager extends EventEmitter {
|
|
|
14106
15411
|
}
|
|
14107
15412
|
// 2. CONTENT: Render all text and elements
|
|
14108
15413
|
const pageIndex = this.document.pages.findIndex(p => p.id === page.id);
|
|
15414
|
+
// Clear content hit targets before rendering all sections (header, body, footer)
|
|
15415
|
+
// so that each section's hit targets are re-registered during render
|
|
15416
|
+
if (pageIndex === 0) {
|
|
15417
|
+
this.flowingTextRenderer.hitTestManager.clearCategory('content');
|
|
15418
|
+
}
|
|
14109
15419
|
// Render header content
|
|
14110
15420
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
14111
15421
|
this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
|
|
@@ -14133,6 +15443,16 @@ class CanvasManager extends EventEmitter {
|
|
|
14133
15443
|
this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
|
|
14134
15444
|
}
|
|
14135
15445
|
}
|
|
15446
|
+
// Render conditional section indicators (only in body)
|
|
15447
|
+
const condSections = bodyFlowingContent?.getConditionalSections() ?? [];
|
|
15448
|
+
if (condSections.length > 0) {
|
|
15449
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
15450
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
15451
|
+
const pageDimensions = page.getPageDimensions();
|
|
15452
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
15453
|
+
this.flowingTextRenderer.renderConditionalSectionIndicators(condSections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedConditionalSectionId);
|
|
15454
|
+
}
|
|
15455
|
+
}
|
|
14136
15456
|
// Render all elements (without selection marks)
|
|
14137
15457
|
this.renderPageElements(page, ctx);
|
|
14138
15458
|
// 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
|
|
@@ -14293,7 +15613,8 @@ class CanvasManager extends EventEmitter {
|
|
|
14293
15613
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
14294
15614
|
// Get the slice for this page (for multi-page tables)
|
|
14295
15615
|
const slice = table.getRenderedSlice(pageIndex);
|
|
14296
|
-
const tablePosition = slice?.position ||
|
|
15616
|
+
const tablePosition = slice?.position ||
|
|
15617
|
+
(table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
|
|
14297
15618
|
const sliceHeight = slice?.height || table.height;
|
|
14298
15619
|
// Check if point is within the table slice on this page
|
|
14299
15620
|
const isInsideTable = tablePosition &&
|
|
@@ -14326,6 +15647,7 @@ class CanvasManager extends EventEmitter {
|
|
|
14326
15647
|
end: cellAddr
|
|
14327
15648
|
});
|
|
14328
15649
|
this.render();
|
|
15650
|
+
this.emit('table-cell-selection-changed', { table });
|
|
14329
15651
|
e.preventDefault();
|
|
14330
15652
|
return;
|
|
14331
15653
|
}
|
|
@@ -14428,11 +15750,10 @@ class CanvasManager extends EventEmitter {
|
|
|
14428
15750
|
const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
|
|
14429
15751
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14430
15752
|
const object = embeddedObjectHit.data.object;
|
|
14431
|
-
//
|
|
15753
|
+
// If object is in a different section, switch to that section first
|
|
14432
15754
|
const objectSection = this.getSectionForEmbeddedObject(object);
|
|
14433
15755
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14434
|
-
|
|
14435
|
-
return;
|
|
15756
|
+
this.setActiveSection(objectSection);
|
|
14436
15757
|
}
|
|
14437
15758
|
// For relative-positioned objects, prepare for potential drag
|
|
14438
15759
|
// Don't start drag immediately - wait for threshold to allow double-click
|
|
@@ -14605,7 +15926,8 @@ class CanvasManager extends EventEmitter {
|
|
|
14605
15926
|
const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
14606
15927
|
// Get the slice for the current page (for multi-page tables)
|
|
14607
15928
|
const slice = table.getRenderedSlice(currentPageIndex);
|
|
14608
|
-
const tablePosition = slice?.position ||
|
|
15929
|
+
const tablePosition = slice?.position ||
|
|
15930
|
+
(table.renderedPageIndex === currentPageIndex ? table.renderedPosition : null);
|
|
14609
15931
|
const sliceHeight = slice?.height || table.height;
|
|
14610
15932
|
if (tablePosition) {
|
|
14611
15933
|
// Check if point is within the table slice on this page
|
|
@@ -14635,6 +15957,7 @@ class CanvasManager extends EventEmitter {
|
|
|
14635
15957
|
end: cellAddr
|
|
14636
15958
|
});
|
|
14637
15959
|
this.render();
|
|
15960
|
+
this.emit('table-cell-selection-changed', { table });
|
|
14638
15961
|
}
|
|
14639
15962
|
}
|
|
14640
15963
|
}
|
|
@@ -14917,14 +16240,12 @@ class CanvasManager extends EventEmitter {
|
|
|
14917
16240
|
const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
|
|
14918
16241
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14919
16242
|
const clickedObject = embeddedObjectHit.data.object;
|
|
14920
|
-
//
|
|
16243
|
+
// If object is in a different section, switch to that section first
|
|
14921
16244
|
const objectSection = this.getSectionForEmbeddedObject(clickedObject);
|
|
14922
|
-
// Only allow selection if object is in the active section
|
|
14923
16245
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14924
|
-
|
|
14925
|
-
return;
|
|
16246
|
+
this.setActiveSection(objectSection);
|
|
14926
16247
|
}
|
|
14927
|
-
// Clicked on embedded object
|
|
16248
|
+
// Clicked on embedded object - clear text selection and select it
|
|
14928
16249
|
const activeFlowingContent = this.getFlowingContentForActiveSection();
|
|
14929
16250
|
if (activeFlowingContent) {
|
|
14930
16251
|
activeFlowingContent.clearSelection();
|
|
@@ -14961,6 +16282,64 @@ class CanvasManager extends EventEmitter {
|
|
|
14961
16282
|
}
|
|
14962
16283
|
}
|
|
14963
16284
|
}
|
|
16285
|
+
// Check if we clicked on a conditional section indicator
|
|
16286
|
+
if (bodyFlowingContent) {
|
|
16287
|
+
const condSections = bodyFlowingContent.getConditionalSections();
|
|
16288
|
+
if (condSections.length > 0 && page) {
|
|
16289
|
+
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
16290
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
16291
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
16292
|
+
const contentBounds = page.getContentBounds();
|
|
16293
|
+
const contentRect = {
|
|
16294
|
+
x: contentBounds.position.x,
|
|
16295
|
+
y: contentBounds.position.y,
|
|
16296
|
+
width: contentBounds.size.width,
|
|
16297
|
+
height: contentBounds.size.height
|
|
16298
|
+
};
|
|
16299
|
+
const pageDimensions = page.getPageDimensions();
|
|
16300
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
16301
|
+
const clickedCondSection = this.flowingTextRenderer.getConditionalSectionAtPoint(point, condSections, pageIndex, pageBounds, contentRect, flowedPages[pageIndex]);
|
|
16302
|
+
if (clickedCondSection) {
|
|
16303
|
+
this.clearSelection();
|
|
16304
|
+
this.selectedConditionalSectionId = clickedCondSection.id;
|
|
16305
|
+
this.render();
|
|
16306
|
+
this.emit('conditional-section-clicked', { section: clickedCondSection });
|
|
16307
|
+
return;
|
|
16308
|
+
}
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
// Check if we clicked on a table row loop label
|
|
16313
|
+
const clickedPageIdx = this.document.pages.findIndex(p => p.id === pageId);
|
|
16314
|
+
const bodyContent = this.document.bodyFlowingContent;
|
|
16315
|
+
if (bodyContent) {
|
|
16316
|
+
const embeddedObjects = bodyContent.getEmbeddedObjects();
|
|
16317
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
16318
|
+
if (obj instanceof TableObject && obj.renderedPosition && obj.renderedPageIndex === clickedPageIdx) {
|
|
16319
|
+
// Convert to table-local coordinates
|
|
16320
|
+
const localPoint = {
|
|
16321
|
+
x: point.x - obj.renderedPosition.x,
|
|
16322
|
+
y: point.y - obj.renderedPosition.y
|
|
16323
|
+
};
|
|
16324
|
+
const clickedLoop = obj.getRowLoopAtPoint(localPoint);
|
|
16325
|
+
if (clickedLoop) {
|
|
16326
|
+
// Select this loop
|
|
16327
|
+
obj.selectRowLoop(clickedLoop.id);
|
|
16328
|
+
this.render();
|
|
16329
|
+
this.emit('table-row-loop-clicked', { table: obj, loop: clickedLoop });
|
|
16330
|
+
return;
|
|
16331
|
+
}
|
|
16332
|
+
// Check for row conditional click
|
|
16333
|
+
const clickedCond = obj.getRowConditionalAtPoint(localPoint);
|
|
16334
|
+
if (clickedCond) {
|
|
16335
|
+
obj.selectRowConditional(clickedCond.id);
|
|
16336
|
+
this.render();
|
|
16337
|
+
this.emit('table-row-conditional-clicked', { table: obj, conditional: clickedCond });
|
|
16338
|
+
return;
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
}
|
|
16342
|
+
}
|
|
14964
16343
|
// If no regular element was clicked, try flowing text using unified region click handler
|
|
14965
16344
|
const ctx = this.contexts.get(pageId);
|
|
14966
16345
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
@@ -15170,48 +16549,49 @@ class CanvasManager extends EventEmitter {
|
|
|
15170
16549
|
const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
|
|
15171
16550
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
15172
16551
|
const object = embeddedObjectHit.data.object;
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
16552
|
+
if (object.position === 'relative') {
|
|
16553
|
+
canvas.style.cursor = 'move';
|
|
16554
|
+
return;
|
|
16555
|
+
}
|
|
16556
|
+
// Show text cursor for objects in edit mode, arrow otherwise
|
|
16557
|
+
if (object instanceof TextBoxObject && this.editingTextBox === object) {
|
|
16558
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16559
|
+
}
|
|
16560
|
+
else if (object instanceof TableObject && this._focusedControl === object) {
|
|
16561
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16562
|
+
}
|
|
15176
16563
|
else {
|
|
15177
|
-
|
|
15178
|
-
canvas.style.cursor = 'move';
|
|
15179
|
-
return;
|
|
15180
|
-
}
|
|
15181
|
-
// Show text cursor for text boxes
|
|
15182
|
-
if (object instanceof TextBoxObject) {
|
|
15183
|
-
canvas.style.cursor = 'text';
|
|
15184
|
-
return;
|
|
15185
|
-
}
|
|
16564
|
+
canvas.style.cursor = 'default';
|
|
15186
16565
|
}
|
|
16566
|
+
return;
|
|
15187
16567
|
}
|
|
15188
16568
|
// Check for table cells (show text cursor)
|
|
15189
16569
|
const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
|
|
15190
16570
|
if (tableCellHit && tableCellHit.data.type === 'table-cell') {
|
|
15191
|
-
canvas.style.cursor =
|
|
16571
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15192
16572
|
return;
|
|
15193
16573
|
}
|
|
15194
16574
|
// Check for text regions (body, header, footer - show text cursor)
|
|
15195
16575
|
const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
|
|
15196
16576
|
if (textRegionHit && textRegionHit.data.type === 'text-region') {
|
|
15197
|
-
canvas.style.cursor =
|
|
16577
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15198
16578
|
return;
|
|
15199
16579
|
}
|
|
15200
16580
|
// Also check if point is within any editable region (body, header, footer)
|
|
15201
16581
|
// This catches cases where text region hit targets may not cover empty space
|
|
15202
16582
|
const bodyRegion = this.regionManager.getBodyRegion();
|
|
15203
16583
|
if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
|
|
15204
|
-
canvas.style.cursor =
|
|
16584
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15205
16585
|
return;
|
|
15206
16586
|
}
|
|
15207
16587
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
15208
16588
|
if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
|
|
15209
|
-
canvas.style.cursor =
|
|
16589
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15210
16590
|
return;
|
|
15211
16591
|
}
|
|
15212
16592
|
const footerRegion = this.regionManager.getFooterRegion();
|
|
15213
16593
|
if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
|
|
15214
|
-
canvas.style.cursor =
|
|
16594
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15215
16595
|
return;
|
|
15216
16596
|
}
|
|
15217
16597
|
canvas.style.cursor = 'default';
|
|
@@ -15229,7 +16609,8 @@ class CanvasManager extends EventEmitter {
|
|
|
15229
16609
|
const { table, dividerType, index } = target.data;
|
|
15230
16610
|
// Get the table position from slice info
|
|
15231
16611
|
const slice = table.getRenderedSlice(pageIndex);
|
|
15232
|
-
const tablePosition = slice?.position ||
|
|
16612
|
+
const tablePosition = slice?.position ||
|
|
16613
|
+
(table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
|
|
15233
16614
|
if (tablePosition) {
|
|
15234
16615
|
// Calculate the divider position based on type and index
|
|
15235
16616
|
let position;
|
|
@@ -15372,6 +16753,7 @@ class CanvasManager extends EventEmitter {
|
|
|
15372
16753
|
});
|
|
15373
16754
|
this.selectedElements.clear();
|
|
15374
16755
|
this.selectedSectionId = null;
|
|
16756
|
+
this.selectedConditionalSectionId = null;
|
|
15375
16757
|
Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
|
|
15376
16758
|
this.render();
|
|
15377
16759
|
this.updateResizeHandleHitTargets();
|
|
@@ -15957,7 +17339,9 @@ class CanvasManager extends EventEmitter {
|
|
|
15957
17339
|
if (obj instanceof TableObject) {
|
|
15958
17340
|
// For multi-page tables, check if this page has a rendered slice
|
|
15959
17341
|
const slice = obj.getRenderedSlice(pageIndex);
|
|
15960
|
-
|
|
17342
|
+
// Only use renderedPosition if the table was actually rendered on this page
|
|
17343
|
+
const tablePosition = slice?.position ||
|
|
17344
|
+
(obj.renderedPageIndex === pageIndex ? obj.renderedPosition : null);
|
|
15961
17345
|
if (tablePosition) {
|
|
15962
17346
|
// Check if point is inside the table slice on this page
|
|
15963
17347
|
const sliceHeight = slice?.height || obj.height;
|
|
@@ -16201,6 +17585,10 @@ class CanvasManager extends EventEmitter {
|
|
|
16201
17585
|
}
|
|
16202
17586
|
CanvasManager.CELL_SELECTION_THRESHOLD = 5; // Minimum pixels to drag before cell selection starts
|
|
16203
17587
|
CanvasManager.RELATIVE_DRAG_THRESHOLD = 3; // Minimum pixels to drag before moving starts
|
|
17588
|
+
// Custom text cursor as a black I-beam SVG data URI.
|
|
17589
|
+
// The native 'text' cursor can render as white on Windows browsers,
|
|
17590
|
+
// making it invisible over the white canvas background.
|
|
17591
|
+
CanvasManager.TEXT_CURSOR = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='20' viewBox='0 0 16 20'%3E%3Cpath d='M5 1h2v1h2V1h2v2h-2v6h2v2h-2v6h2v2h-2v-1H7v1H5v-2h2v-6H5V9h2V3H5z' fill='%23000'/%3E%3C/svg%3E\") 8 10, text";
|
|
16204
17592
|
|
|
16205
17593
|
/**
|
|
16206
17594
|
* DataBinder handles binding data to documents.
|
|
@@ -16448,8 +17836,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
|
|
|
16448
17836
|
* - Repeating section indicators, loop markers
|
|
16449
17837
|
*/
|
|
16450
17838
|
class PDFGenerator {
|
|
16451
|
-
constructor() {
|
|
17839
|
+
constructor(fontManager) {
|
|
16452
17840
|
this.fontCache = new Map();
|
|
17841
|
+
this.customFontCache = new Map();
|
|
17842
|
+
this.fontManager = fontManager;
|
|
16453
17843
|
}
|
|
16454
17844
|
/**
|
|
16455
17845
|
* Generate a PDF from the document.
|
|
@@ -16460,9 +17850,13 @@ class PDFGenerator {
|
|
|
16460
17850
|
*/
|
|
16461
17851
|
async generate(document, flowedContent, _options) {
|
|
16462
17852
|
const pdfDoc = await PDFDocument.create();
|
|
17853
|
+
pdfDoc.registerFontkit(fontkit);
|
|
16463
17854
|
this.fontCache.clear();
|
|
17855
|
+
this.customFontCache.clear();
|
|
16464
17856
|
// Embed standard fonts we'll need
|
|
16465
17857
|
await this.embedStandardFonts(pdfDoc);
|
|
17858
|
+
// Embed any custom fonts that have font data
|
|
17859
|
+
await this.embedCustomFonts(pdfDoc);
|
|
16466
17860
|
// Render each page
|
|
16467
17861
|
for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
|
|
16468
17862
|
try {
|
|
@@ -16584,11 +17978,59 @@ class PDFGenerator {
|
|
|
16584
17978
|
}
|
|
16585
17979
|
return result;
|
|
16586
17980
|
}
|
|
17981
|
+
/**
|
|
17982
|
+
* Embed custom fonts that have raw font data available.
|
|
17983
|
+
*/
|
|
17984
|
+
async embedCustomFonts(pdfDoc) {
|
|
17985
|
+
const fonts = this.fontManager.getAvailableFonts();
|
|
17986
|
+
for (const font of fonts) {
|
|
17987
|
+
if (font.source !== 'custom')
|
|
17988
|
+
continue;
|
|
17989
|
+
for (const variant of font.variants) {
|
|
17990
|
+
if (!variant.fontData)
|
|
17991
|
+
continue;
|
|
17992
|
+
const cacheKey = `custom:${font.family.toLowerCase()}:${variant.weight}:${variant.style}`;
|
|
17993
|
+
try {
|
|
17994
|
+
// Ensure we pass Uint8Array (some pdf-lib versions need it)
|
|
17995
|
+
const fontBytes = variant.fontData instanceof Uint8Array
|
|
17996
|
+
? variant.fontData
|
|
17997
|
+
: new Uint8Array(variant.fontData);
|
|
17998
|
+
const embedded = await pdfDoc.embedFont(fontBytes, { subset: true });
|
|
17999
|
+
this.customFontCache.set(cacheKey, embedded);
|
|
18000
|
+
Logger.log('[pc-editor:PDFGenerator] Embedded custom font:', font.family, variant.weight, variant.style);
|
|
18001
|
+
}
|
|
18002
|
+
catch (e) {
|
|
18003
|
+
Logger.warn('[pc-editor:PDFGenerator] Failed to embed custom font:', font.family, e);
|
|
18004
|
+
}
|
|
18005
|
+
}
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
18008
|
+
/**
|
|
18009
|
+
* Check if a font family is a custom font with embedded data.
|
|
18010
|
+
*/
|
|
18011
|
+
isCustomFont(family) {
|
|
18012
|
+
return !this.fontManager.isBuiltIn(family) && this.fontManager.isRegistered(family);
|
|
18013
|
+
}
|
|
16587
18014
|
/**
|
|
16588
18015
|
* Get a font from cache by formatting style.
|
|
18016
|
+
* Checks custom fonts first, then falls back to standard fonts.
|
|
16589
18017
|
*/
|
|
16590
18018
|
getFont(formatting) {
|
|
16591
|
-
const
|
|
18019
|
+
const family = formatting.fontFamily || 'Arial';
|
|
18020
|
+
const weight = formatting.fontWeight || 'normal';
|
|
18021
|
+
const style = formatting.fontStyle || 'normal';
|
|
18022
|
+
// Try custom font first
|
|
18023
|
+
const customKey = `custom:${family.toLowerCase()}:${weight}:${style}`;
|
|
18024
|
+
const customFont = this.customFontCache.get(customKey);
|
|
18025
|
+
if (customFont)
|
|
18026
|
+
return customFont;
|
|
18027
|
+
// Try custom font with normal variant as fallback
|
|
18028
|
+
const customNormalKey = `custom:${family.toLowerCase()}:normal:normal`;
|
|
18029
|
+
const customNormalFont = this.customFontCache.get(customNormalKey);
|
|
18030
|
+
if (customNormalFont)
|
|
18031
|
+
return customNormalFont;
|
|
18032
|
+
// Fall back to standard fonts
|
|
18033
|
+
const standardFont = getStandardFont(family, weight, style);
|
|
16592
18034
|
return this.fontCache.get(standardFont) || this.fontCache.get(StandardFonts.Helvetica);
|
|
16593
18035
|
}
|
|
16594
18036
|
/**
|
|
@@ -16621,12 +18063,14 @@ class PDFGenerator {
|
|
|
16621
18063
|
for (const run of line.runs) {
|
|
16622
18064
|
if (!run.text)
|
|
16623
18065
|
continue;
|
|
16624
|
-
// Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
|
|
16625
|
-
const safeText = this.filterToWinAnsi(run.text);
|
|
16626
|
-
if (!safeText)
|
|
16627
|
-
continue;
|
|
16628
18066
|
// Ensure formatting has required properties with defaults
|
|
16629
18067
|
const formatting = run.formatting || {};
|
|
18068
|
+
// Custom fonts support full Unicode; standard fonts need WinAnsi filtering
|
|
18069
|
+
const safeText = this.isCustomFont(formatting.fontFamily || 'Arial')
|
|
18070
|
+
? run.text
|
|
18071
|
+
: this.filterToWinAnsi(run.text);
|
|
18072
|
+
if (!safeText)
|
|
18073
|
+
continue;
|
|
16630
18074
|
const font = this.getFont(formatting);
|
|
16631
18075
|
const fontSize = formatting.fontSize || 14;
|
|
16632
18076
|
const color = parseColor(formatting.color || '#000000');
|
|
@@ -21301,6 +22745,156 @@ class PDFImporter {
|
|
|
21301
22745
|
}
|
|
21302
22746
|
}
|
|
21303
22747
|
|
|
22748
|
+
/**
|
|
22749
|
+
* FontManager - Manages font registration and availability for the editor.
|
|
22750
|
+
*
|
|
22751
|
+
* Built-in fonts are web-safe and map to pdf-lib StandardFonts.
|
|
22752
|
+
* Custom fonts are loaded via the FontFace API for canvas rendering
|
|
22753
|
+
* and their raw bytes are stored for PDF embedding.
|
|
22754
|
+
*/
|
|
22755
|
+
/**
|
|
22756
|
+
* Built-in web-safe fonts that need no loading.
|
|
22757
|
+
*/
|
|
22758
|
+
const BUILT_IN_FONTS = [
|
|
22759
|
+
'Arial',
|
|
22760
|
+
'Times New Roman',
|
|
22761
|
+
'Courier New',
|
|
22762
|
+
'Georgia',
|
|
22763
|
+
'Verdana'
|
|
22764
|
+
];
|
|
22765
|
+
class FontManager extends EventEmitter {
|
|
22766
|
+
constructor() {
|
|
22767
|
+
super();
|
|
22768
|
+
this.fonts = new Map();
|
|
22769
|
+
// Register built-in fonts
|
|
22770
|
+
for (const family of BUILT_IN_FONTS) {
|
|
22771
|
+
this.fonts.set(family.toLowerCase(), {
|
|
22772
|
+
family,
|
|
22773
|
+
source: 'built-in',
|
|
22774
|
+
variants: [{
|
|
22775
|
+
weight: 'normal',
|
|
22776
|
+
style: 'normal',
|
|
22777
|
+
fontData: null,
|
|
22778
|
+
loaded: true
|
|
22779
|
+
}]
|
|
22780
|
+
});
|
|
22781
|
+
}
|
|
22782
|
+
}
|
|
22783
|
+
/**
|
|
22784
|
+
* Register a custom font. Fetches the font data if a URL is provided,
|
|
22785
|
+
* creates a FontFace for canvas rendering, and stores the raw bytes for PDF embedding.
|
|
22786
|
+
*/
|
|
22787
|
+
async registerFont(options) {
|
|
22788
|
+
const { family, url, data, weight = 'normal', style = 'normal' } = options;
|
|
22789
|
+
Logger.log('[pc-editor:FontManager] registerFont', family, weight, style);
|
|
22790
|
+
let fontData = null;
|
|
22791
|
+
// Get font bytes
|
|
22792
|
+
if (data) {
|
|
22793
|
+
fontData = data;
|
|
22794
|
+
}
|
|
22795
|
+
else if (url) {
|
|
22796
|
+
try {
|
|
22797
|
+
const response = await fetch(url);
|
|
22798
|
+
if (!response.ok) {
|
|
22799
|
+
throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`);
|
|
22800
|
+
}
|
|
22801
|
+
fontData = await response.arrayBuffer();
|
|
22802
|
+
}
|
|
22803
|
+
catch (e) {
|
|
22804
|
+
Logger.error(`[pc-editor:FontManager] Failed to fetch font "${family}" from ${url}:`, e);
|
|
22805
|
+
throw e;
|
|
22806
|
+
}
|
|
22807
|
+
}
|
|
22808
|
+
// Create FontFace for canvas rendering
|
|
22809
|
+
if (fontData && typeof FontFace !== 'undefined') {
|
|
22810
|
+
try {
|
|
22811
|
+
const fontFace = new FontFace(family, fontData, {
|
|
22812
|
+
weight,
|
|
22813
|
+
style
|
|
22814
|
+
});
|
|
22815
|
+
await fontFace.load();
|
|
22816
|
+
document.fonts.add(fontFace);
|
|
22817
|
+
Logger.log('[pc-editor:FontManager] FontFace loaded:', family, weight, style);
|
|
22818
|
+
}
|
|
22819
|
+
catch (e) {
|
|
22820
|
+
Logger.error(`[pc-editor:FontManager] Failed to load FontFace "${family}":`, e);
|
|
22821
|
+
throw e;
|
|
22822
|
+
}
|
|
22823
|
+
}
|
|
22824
|
+
// Register in our map
|
|
22825
|
+
const key = family.toLowerCase();
|
|
22826
|
+
let registration = this.fonts.get(key);
|
|
22827
|
+
if (!registration) {
|
|
22828
|
+
registration = {
|
|
22829
|
+
family,
|
|
22830
|
+
source: 'custom',
|
|
22831
|
+
variants: []
|
|
22832
|
+
};
|
|
22833
|
+
this.fonts.set(key, registration);
|
|
22834
|
+
}
|
|
22835
|
+
else if (registration.source === 'built-in') {
|
|
22836
|
+
// Upgrading a built-in font with custom data (e.g., for PDF embedding)
|
|
22837
|
+
registration.source = 'custom';
|
|
22838
|
+
}
|
|
22839
|
+
// Add or update variant
|
|
22840
|
+
const existingVariant = registration.variants.find(v => v.weight === weight && v.style === style);
|
|
22841
|
+
if (existingVariant) {
|
|
22842
|
+
existingVariant.fontData = fontData;
|
|
22843
|
+
existingVariant.loaded = true;
|
|
22844
|
+
}
|
|
22845
|
+
else {
|
|
22846
|
+
registration.variants.push({
|
|
22847
|
+
weight,
|
|
22848
|
+
style,
|
|
22849
|
+
fontData,
|
|
22850
|
+
loaded: true
|
|
22851
|
+
});
|
|
22852
|
+
}
|
|
22853
|
+
this.emit('font-registered', { family, weight, style });
|
|
22854
|
+
}
|
|
22855
|
+
/**
|
|
22856
|
+
* Get all registered font families.
|
|
22857
|
+
*/
|
|
22858
|
+
getAvailableFonts() {
|
|
22859
|
+
return Array.from(this.fonts.values());
|
|
22860
|
+
}
|
|
22861
|
+
/**
|
|
22862
|
+
* Get all available font family names.
|
|
22863
|
+
*/
|
|
22864
|
+
getAvailableFontFamilies() {
|
|
22865
|
+
return Array.from(this.fonts.values()).map(f => f.family);
|
|
22866
|
+
}
|
|
22867
|
+
/**
|
|
22868
|
+
* Check if a font family is built-in.
|
|
22869
|
+
*/
|
|
22870
|
+
isBuiltIn(family) {
|
|
22871
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22872
|
+
return reg?.source === 'built-in';
|
|
22873
|
+
}
|
|
22874
|
+
/**
|
|
22875
|
+
* Check if a font family is registered (built-in or custom).
|
|
22876
|
+
*/
|
|
22877
|
+
isRegistered(family) {
|
|
22878
|
+
return this.fonts.has(family.toLowerCase());
|
|
22879
|
+
}
|
|
22880
|
+
/**
|
|
22881
|
+
* Get raw font bytes for PDF embedding.
|
|
22882
|
+
* Returns null for built-in fonts (they use StandardFonts in pdf-lib).
|
|
22883
|
+
*/
|
|
22884
|
+
getFontData(family, weight = 'normal', style = 'normal') {
|
|
22885
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22886
|
+
if (!reg)
|
|
22887
|
+
return null;
|
|
22888
|
+
// Try exact match first
|
|
22889
|
+
const exact = reg.variants.find(v => v.weight === weight && v.style === style);
|
|
22890
|
+
if (exact?.fontData)
|
|
22891
|
+
return exact.fontData;
|
|
22892
|
+
// Fall back to normal variant
|
|
22893
|
+
const normal = reg.variants.find(v => v.weight === 'normal' && v.style === 'normal');
|
|
22894
|
+
return normal?.fontData || null;
|
|
22895
|
+
}
|
|
22896
|
+
}
|
|
22897
|
+
|
|
21304
22898
|
class PCEditor extends EventEmitter {
|
|
21305
22899
|
constructor(container, options) {
|
|
21306
22900
|
super();
|
|
@@ -21327,7 +22921,8 @@ class PCEditor extends EventEmitter {
|
|
|
21327
22921
|
units: this.options.units
|
|
21328
22922
|
});
|
|
21329
22923
|
this.dataBinder = new DataBinder();
|
|
21330
|
-
this.
|
|
22924
|
+
this.fontManager = new FontManager();
|
|
22925
|
+
this.pdfGenerator = new PDFGenerator(this.fontManager);
|
|
21331
22926
|
this.clipboardManager = new ClipboardManager();
|
|
21332
22927
|
this.initialize();
|
|
21333
22928
|
}
|
|
@@ -21489,6 +23084,14 @@ class PCEditor extends EventEmitter {
|
|
|
21489
23084
|
this.canvasManager.on('tablecell-cursor-changed', (data) => {
|
|
21490
23085
|
this.emit('tablecell-cursor-changed', data);
|
|
21491
23086
|
});
|
|
23087
|
+
// Forward table cell selection changes (multi-cell drag/shift-click)
|
|
23088
|
+
this.canvasManager.on('table-cell-selection-changed', (data) => {
|
|
23089
|
+
this.emit('table-cell-selection-changed', data);
|
|
23090
|
+
});
|
|
23091
|
+
// Forward table row loop clicks
|
|
23092
|
+
this.canvasManager.on('table-row-loop-clicked', (data) => {
|
|
23093
|
+
this.emit('table-row-loop-clicked', data);
|
|
23094
|
+
});
|
|
21492
23095
|
this.canvasManager.on('repeating-section-clicked', (data) => {
|
|
21493
23096
|
// Repeating section clicked - update selection state
|
|
21494
23097
|
if (data.section && data.section.id) {
|
|
@@ -21499,6 +23102,16 @@ class PCEditor extends EventEmitter {
|
|
|
21499
23102
|
this.emitSelectionChange();
|
|
21500
23103
|
}
|
|
21501
23104
|
});
|
|
23105
|
+
this.canvasManager.on('conditional-section-clicked', (data) => {
|
|
23106
|
+
// Conditional section clicked - update selection state
|
|
23107
|
+
if (data.section && data.section.id) {
|
|
23108
|
+
this.currentSelection = {
|
|
23109
|
+
type: 'conditional-section',
|
|
23110
|
+
sectionId: data.section.id
|
|
23111
|
+
};
|
|
23112
|
+
this.emitSelectionChange();
|
|
23113
|
+
}
|
|
23114
|
+
});
|
|
21502
23115
|
// Listen for section focus changes from CanvasManager (double-click)
|
|
21503
23116
|
this.canvasManager.on('section-focus-changed', (data) => {
|
|
21504
23117
|
// Update our internal state to match the canvas manager
|
|
@@ -22327,17 +23940,24 @@ class PCEditor extends EventEmitter {
|
|
|
22327
23940
|
this.selectAll();
|
|
22328
23941
|
return;
|
|
22329
23942
|
}
|
|
22330
|
-
// If an embedded object is selected (but not being edited),
|
|
22331
|
-
|
|
22332
|
-
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
22333
|
-
if (isArrowKey && this.canvasManager.hasSelectedElements()) {
|
|
22334
|
-
// Check if we're not in editing mode
|
|
23943
|
+
// If an embedded object is selected (but not being edited), handle special keys
|
|
23944
|
+
if (this.canvasManager.hasSelectedElements()) {
|
|
22335
23945
|
const editingTextBox = this.canvasManager.getEditingTextBox();
|
|
22336
23946
|
const focusedTable = this.canvasManager.getFocusedControl();
|
|
22337
23947
|
const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
|
|
22338
23948
|
if (!isEditing) {
|
|
22339
|
-
//
|
|
22340
|
-
|
|
23949
|
+
// Arrow keys: deselect and move cursor in text flow
|
|
23950
|
+
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
23951
|
+
if (isArrowKey) {
|
|
23952
|
+
this.canvasManager.clearSelection();
|
|
23953
|
+
// Fall through to normal key handling
|
|
23954
|
+
}
|
|
23955
|
+
// Backspace/Delete: delete the selected object from the text flow
|
|
23956
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
23957
|
+
e.preventDefault();
|
|
23958
|
+
this.deleteSelectedElements();
|
|
23959
|
+
return;
|
|
23960
|
+
}
|
|
22341
23961
|
}
|
|
22342
23962
|
}
|
|
22343
23963
|
// Use the unified focus system to get the currently focused control
|
|
@@ -22440,6 +24060,32 @@ class PCEditor extends EventEmitter {
|
|
|
22440
24060
|
this.canvasManager.clearSelection();
|
|
22441
24061
|
this.canvasManager.render();
|
|
22442
24062
|
}
|
|
24063
|
+
/**
|
|
24064
|
+
* Delete all currently selected embedded objects from the text flow.
|
|
24065
|
+
*/
|
|
24066
|
+
deleteSelectedElements() {
|
|
24067
|
+
const selectedElements = this.canvasManager.getSelectedElements();
|
|
24068
|
+
if (selectedElements.length === 0)
|
|
24069
|
+
return;
|
|
24070
|
+
for (const elementId of selectedElements) {
|
|
24071
|
+
const objectInfo = this.findEmbeddedObjectInfo(elementId);
|
|
24072
|
+
if (objectInfo) {
|
|
24073
|
+
// Delete the placeholder character at the object's text index
|
|
24074
|
+
// This removes the object from the text flow
|
|
24075
|
+
objectInfo.content.deleteText(objectInfo.textIndex, 1);
|
|
24076
|
+
// Return focus to the parent flowing content
|
|
24077
|
+
const cursorPos = Math.min(objectInfo.textIndex, objectInfo.content.getText().length);
|
|
24078
|
+
objectInfo.content.setCursorPosition(cursorPos);
|
|
24079
|
+
this.canvasManager.setFocus(objectInfo.content);
|
|
24080
|
+
if (objectInfo.section !== this.canvasManager.getActiveSection()) {
|
|
24081
|
+
this.canvasManager.setActiveSection(objectInfo.section);
|
|
24082
|
+
}
|
|
24083
|
+
}
|
|
24084
|
+
}
|
|
24085
|
+
this.canvasManager.clearSelection();
|
|
24086
|
+
this.canvasManager.render();
|
|
24087
|
+
this.emit('content-changed', {});
|
|
24088
|
+
}
|
|
22443
24089
|
/**
|
|
22444
24090
|
* Find embedded object info by ID across all flowing content sources.
|
|
22445
24091
|
*/
|
|
@@ -23458,6 +25104,39 @@ class PCEditor extends EventEmitter {
|
|
|
23458
25104
|
table.removeColumn(colIndex);
|
|
23459
25105
|
this.canvasManager.render();
|
|
23460
25106
|
}
|
|
25107
|
+
/**
|
|
25108
|
+
* Merge selected cells in a table.
|
|
25109
|
+
* Uses the table's current cell selection range.
|
|
25110
|
+
* @param table The table containing the cells to merge
|
|
25111
|
+
* @returns true if cells were merged successfully
|
|
25112
|
+
*/
|
|
25113
|
+
tableMergeCells(table) {
|
|
25114
|
+
Logger.log('[pc-editor] tableMergeCells');
|
|
25115
|
+
if (!this._isReady)
|
|
25116
|
+
return false;
|
|
25117
|
+
const result = table.mergeCells();
|
|
25118
|
+
if (result.success) {
|
|
25119
|
+
this.canvasManager.render();
|
|
25120
|
+
}
|
|
25121
|
+
return result.success;
|
|
25122
|
+
}
|
|
25123
|
+
/**
|
|
25124
|
+
* Split a merged cell back into individual cells.
|
|
25125
|
+
* @param table The table containing the merged cell
|
|
25126
|
+
* @param row Row index of the merged cell
|
|
25127
|
+
* @param col Column index of the merged cell
|
|
25128
|
+
* @returns true if the cell was split successfully
|
|
25129
|
+
*/
|
|
25130
|
+
tableSplitCell(table, row, col) {
|
|
25131
|
+
Logger.log('[pc-editor] tableSplitCell', row, col);
|
|
25132
|
+
if (!this._isReady)
|
|
25133
|
+
return false;
|
|
25134
|
+
const result = table.splitCell(row, col);
|
|
25135
|
+
if (result.success) {
|
|
25136
|
+
this.canvasManager.render();
|
|
25137
|
+
}
|
|
25138
|
+
return result.success;
|
|
25139
|
+
}
|
|
23461
25140
|
/**
|
|
23462
25141
|
* Begin a compound operation. Groups multiple mutations into a single undo entry.
|
|
23463
25142
|
* Call endCompoundOperation after making changes.
|
|
@@ -23524,11 +25203,17 @@ class PCEditor extends EventEmitter {
|
|
|
23524
25203
|
let totalFieldCount = 0;
|
|
23525
25204
|
// Step 1: Expand repeating sections in body (header/footer don't support them)
|
|
23526
25205
|
this.expandRepeatingSections(bodyContent, data);
|
|
23527
|
-
// Step 2:
|
|
25206
|
+
// Step 2: Evaluate conditional sections in body (remove content where predicate is false)
|
|
25207
|
+
this.evaluateConditionalSections(bodyContent, data);
|
|
25208
|
+
// Step 3: Expand table row loops in body, header, and footer
|
|
23528
25209
|
this.expandTableRowLoops(bodyContent, data);
|
|
23529
25210
|
this.expandTableRowLoops(this.document.headerFlowingContent, data);
|
|
23530
25211
|
this.expandTableRowLoops(this.document.footerFlowingContent, data);
|
|
23531
|
-
// Step
|
|
25212
|
+
// Step 4: Evaluate table row conditionals in body, header, and footer
|
|
25213
|
+
this.evaluateTableRowConditionals(bodyContent, data);
|
|
25214
|
+
this.evaluateTableRowConditionals(this.document.headerFlowingContent, data);
|
|
25215
|
+
this.evaluateTableRowConditionals(this.document.footerFlowingContent, data);
|
|
25216
|
+
// Step 5: Substitute all fields in body
|
|
23532
25217
|
totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
|
|
23533
25218
|
// Step 4: Substitute all fields in embedded objects in body
|
|
23534
25219
|
totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
|
|
@@ -23736,9 +25421,62 @@ class PCEditor extends EventEmitter {
|
|
|
23736
25421
|
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
23737
25422
|
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
23738
25423
|
}
|
|
23739
|
-
// Remove the section after expansion
|
|
23740
|
-
sectionManager.remove(section.id);
|
|
25424
|
+
// Remove the section after expansion
|
|
25425
|
+
sectionManager.remove(section.id);
|
|
25426
|
+
}
|
|
25427
|
+
}
|
|
25428
|
+
/**
|
|
25429
|
+
* Evaluate conditional sections by removing content where predicate is false.
|
|
25430
|
+
* Processes sections from end to start to preserve text indices.
|
|
25431
|
+
*/
|
|
25432
|
+
evaluateConditionalSections(flowingContent, data) {
|
|
25433
|
+
const sectionManager = flowingContent.getConditionalSectionManager();
|
|
25434
|
+
// Get sections in descending order (process end-to-start)
|
|
25435
|
+
const sections = sectionManager.getSectionsDescending();
|
|
25436
|
+
for (const section of sections) {
|
|
25437
|
+
const result = PredicateEvaluator.evaluate(section.predicate, data);
|
|
25438
|
+
if (!result) {
|
|
25439
|
+
// Predicate is false — remove the content within this section
|
|
25440
|
+
const deleteStart = section.startIndex;
|
|
25441
|
+
const deleteLength = section.endIndex - section.startIndex;
|
|
25442
|
+
flowingContent.deleteText(deleteStart, deleteLength);
|
|
25443
|
+
}
|
|
25444
|
+
// Remove the conditional section marker regardless
|
|
25445
|
+
sectionManager.remove(section.id);
|
|
25446
|
+
}
|
|
25447
|
+
}
|
|
25448
|
+
/**
|
|
25449
|
+
* Evaluate table row conditionals in embedded tables within a FlowingTextContent.
|
|
25450
|
+
* For each table with row conditionals, removes rows where predicate is false.
|
|
25451
|
+
*/
|
|
25452
|
+
evaluateTableRowConditionals(flowingContent, data) {
|
|
25453
|
+
const embeddedObjects = flowingContent.getEmbeddedObjects();
|
|
25454
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
25455
|
+
if (obj instanceof TableObject) {
|
|
25456
|
+
this.evaluateTableRowConditionalsInTable(obj, data);
|
|
25457
|
+
}
|
|
25458
|
+
}
|
|
25459
|
+
}
|
|
25460
|
+
/**
|
|
25461
|
+
* Evaluate row conditionals in a single table.
|
|
25462
|
+
* Processes conditionals from end to start to preserve row indices.
|
|
25463
|
+
*/
|
|
25464
|
+
evaluateTableRowConditionalsInTable(table, data) {
|
|
25465
|
+
const conditionals = table.getAllRowConditionals();
|
|
25466
|
+
if (conditionals.length === 0)
|
|
25467
|
+
return;
|
|
25468
|
+
// Sort by startRowIndex descending (process end-to-start)
|
|
25469
|
+
const sorted = [...conditionals].sort((a, b) => b.startRowIndex - a.startRowIndex);
|
|
25470
|
+
for (const cond of sorted) {
|
|
25471
|
+
const result = PredicateEvaluator.evaluate(cond.predicate, data);
|
|
25472
|
+
if (!result) {
|
|
25473
|
+
// Predicate is false — remove the rows
|
|
25474
|
+
table.removeRowsInRange(cond.startRowIndex, cond.endRowIndex);
|
|
25475
|
+
}
|
|
25476
|
+
// Remove the conditional marker regardless
|
|
25477
|
+
table.removeRowConditional(cond.id);
|
|
23741
25478
|
}
|
|
25479
|
+
table.markLayoutDirty();
|
|
23742
25480
|
}
|
|
23743
25481
|
/**
|
|
23744
25482
|
* Get a value at a path without array defaulting.
|
|
@@ -23987,7 +25725,7 @@ class PCEditor extends EventEmitter {
|
|
|
23987
25725
|
toggleBulletList() {
|
|
23988
25726
|
if (!this._isReady)
|
|
23989
25727
|
return;
|
|
23990
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25728
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
23991
25729
|
if (!flowingContent)
|
|
23992
25730
|
return;
|
|
23993
25731
|
flowingContent.toggleBulletList();
|
|
@@ -24000,7 +25738,7 @@ class PCEditor extends EventEmitter {
|
|
|
24000
25738
|
toggleNumberedList() {
|
|
24001
25739
|
if (!this._isReady)
|
|
24002
25740
|
return;
|
|
24003
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25741
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24004
25742
|
if (!flowingContent)
|
|
24005
25743
|
return;
|
|
24006
25744
|
flowingContent.toggleNumberedList();
|
|
@@ -24013,7 +25751,7 @@ class PCEditor extends EventEmitter {
|
|
|
24013
25751
|
indentParagraph() {
|
|
24014
25752
|
if (!this._isReady)
|
|
24015
25753
|
return;
|
|
24016
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25754
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24017
25755
|
if (!flowingContent)
|
|
24018
25756
|
return;
|
|
24019
25757
|
flowingContent.indentParagraph();
|
|
@@ -24026,7 +25764,7 @@ class PCEditor extends EventEmitter {
|
|
|
24026
25764
|
outdentParagraph() {
|
|
24027
25765
|
if (!this._isReady)
|
|
24028
25766
|
return;
|
|
24029
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25767
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24030
25768
|
if (!flowingContent)
|
|
24031
25769
|
return;
|
|
24032
25770
|
flowingContent.outdentParagraph();
|
|
@@ -24039,7 +25777,7 @@ class PCEditor extends EventEmitter {
|
|
|
24039
25777
|
getListFormatting() {
|
|
24040
25778
|
if (!this._isReady)
|
|
24041
25779
|
return undefined;
|
|
24042
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25780
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24043
25781
|
if (!flowingContent)
|
|
24044
25782
|
return undefined;
|
|
24045
25783
|
return flowingContent.getListFormatting();
|
|
@@ -24250,9 +25988,12 @@ class PCEditor extends EventEmitter {
|
|
|
24250
25988
|
// If a table is focused, create a row loop instead of a text repeating section
|
|
24251
25989
|
const focusedTable = this.getFocusedTable();
|
|
24252
25990
|
if (focusedTable && focusedTable.focusedCell) {
|
|
24253
|
-
|
|
24254
|
-
const
|
|
24255
|
-
const
|
|
25991
|
+
// Use the selected range if multiple rows are selected, otherwise use the focused cell's row
|
|
25992
|
+
const selectedRange = focusedTable.selectedRange;
|
|
25993
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
25994
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
25995
|
+
Logger.log('[pc-editor] createRepeatingSection → table row loop', startRow, endRow, fieldPath);
|
|
25996
|
+
const loop = focusedTable.createRowLoop(startRow, endRow, fieldPath);
|
|
24256
25997
|
if (loop) {
|
|
24257
25998
|
this.canvasManager.render();
|
|
24258
25999
|
this.emit('table-row-loop-added', { table: focusedTable, loop });
|
|
@@ -24324,6 +26065,103 @@ class PCEditor extends EventEmitter {
|
|
|
24324
26065
|
return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
|
|
24325
26066
|
}
|
|
24326
26067
|
// ============================================
|
|
26068
|
+
// Conditional Section API
|
|
26069
|
+
// ============================================
|
|
26070
|
+
/**
|
|
26071
|
+
* Create a conditional section.
|
|
26072
|
+
*
|
|
26073
|
+
* If a table is currently being edited (focused), creates a table row conditional
|
|
26074
|
+
* based on the focused cell's row.
|
|
26075
|
+
*
|
|
26076
|
+
* Otherwise, creates a body text conditional section at the given paragraph boundaries.
|
|
26077
|
+
*
|
|
26078
|
+
* @param startIndex Text index at paragraph start (ignored for table row conditionals)
|
|
26079
|
+
* @param endIndex Text index at closing paragraph start (ignored for table row conditionals)
|
|
26080
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
26081
|
+
* @returns The created section, or null if creation failed
|
|
26082
|
+
*/
|
|
26083
|
+
addConditionalSection(startIndex, endIndex, predicate) {
|
|
26084
|
+
if (!this._isReady) {
|
|
26085
|
+
throw new Error('Editor is not ready');
|
|
26086
|
+
}
|
|
26087
|
+
// If a table is focused, create a row conditional instead
|
|
26088
|
+
const focusedTable = this.getFocusedTable();
|
|
26089
|
+
if (focusedTable && focusedTable.focusedCell) {
|
|
26090
|
+
const selectedRange = focusedTable.selectedRange;
|
|
26091
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
26092
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
26093
|
+
Logger.log('[pc-editor] addConditionalSection → table row conditional', startRow, endRow, predicate);
|
|
26094
|
+
const cond = focusedTable.createRowConditional(startRow, endRow, predicate);
|
|
26095
|
+
if (cond) {
|
|
26096
|
+
this.canvasManager.render();
|
|
26097
|
+
this.emit('table-row-conditional-added', { table: focusedTable, conditional: cond });
|
|
26098
|
+
}
|
|
26099
|
+
return null; // Row conditionals are not ConditionalSections, return null
|
|
26100
|
+
}
|
|
26101
|
+
Logger.log('[pc-editor] addConditionalSection', startIndex, endIndex, predicate);
|
|
26102
|
+
const section = this.document.bodyFlowingContent.createConditionalSection(startIndex, endIndex, predicate);
|
|
26103
|
+
if (section) {
|
|
26104
|
+
this.canvasManager.render();
|
|
26105
|
+
this.emit('conditional-section-added', { section });
|
|
26106
|
+
}
|
|
26107
|
+
return section;
|
|
26108
|
+
}
|
|
26109
|
+
/**
|
|
26110
|
+
* Get a conditional section by ID.
|
|
26111
|
+
*/
|
|
26112
|
+
getConditionalSection(id) {
|
|
26113
|
+
if (!this._isReady) {
|
|
26114
|
+
return null;
|
|
26115
|
+
}
|
|
26116
|
+
return this.document.bodyFlowingContent.getConditionalSection(id) || null;
|
|
26117
|
+
}
|
|
26118
|
+
/**
|
|
26119
|
+
* Get all conditional sections.
|
|
26120
|
+
*/
|
|
26121
|
+
getConditionalSections() {
|
|
26122
|
+
if (!this._isReady) {
|
|
26123
|
+
return [];
|
|
26124
|
+
}
|
|
26125
|
+
return this.document.bodyFlowingContent.getConditionalSections();
|
|
26126
|
+
}
|
|
26127
|
+
/**
|
|
26128
|
+
* Update a conditional section's predicate.
|
|
26129
|
+
*/
|
|
26130
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
26131
|
+
if (!this._isReady) {
|
|
26132
|
+
return false;
|
|
26133
|
+
}
|
|
26134
|
+
const success = this.document.bodyFlowingContent.updateConditionalSectionPredicate(id, predicate);
|
|
26135
|
+
if (success) {
|
|
26136
|
+
this.canvasManager.render();
|
|
26137
|
+
this.emit('conditional-section-updated', { id, predicate });
|
|
26138
|
+
}
|
|
26139
|
+
return success;
|
|
26140
|
+
}
|
|
26141
|
+
/**
|
|
26142
|
+
* Remove a conditional section by ID.
|
|
26143
|
+
*/
|
|
26144
|
+
removeConditionalSection(id) {
|
|
26145
|
+
if (!this._isReady) {
|
|
26146
|
+
return false;
|
|
26147
|
+
}
|
|
26148
|
+
const success = this.document.bodyFlowingContent.removeConditionalSection(id);
|
|
26149
|
+
if (success) {
|
|
26150
|
+
this.canvasManager.render();
|
|
26151
|
+
this.emit('conditional-section-removed', { id });
|
|
26152
|
+
}
|
|
26153
|
+
return success;
|
|
26154
|
+
}
|
|
26155
|
+
/**
|
|
26156
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
26157
|
+
*/
|
|
26158
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
26159
|
+
if (!this._isReady) {
|
|
26160
|
+
return null;
|
|
26161
|
+
}
|
|
26162
|
+
return this.document.bodyFlowingContent.getConditionalSectionAtBoundary(textIndex) || null;
|
|
26163
|
+
}
|
|
26164
|
+
// ============================================
|
|
24327
26165
|
// Header/Footer API
|
|
24328
26166
|
// ============================================
|
|
24329
26167
|
/**
|
|
@@ -24674,6 +26512,39 @@ class PCEditor extends EventEmitter {
|
|
|
24674
26512
|
setLogging(enabled) {
|
|
24675
26513
|
Logger.setEnabled(enabled);
|
|
24676
26514
|
}
|
|
26515
|
+
// ============================================
|
|
26516
|
+
// Font Management
|
|
26517
|
+
// ============================================
|
|
26518
|
+
/**
|
|
26519
|
+
* Register a custom font for use in the editor and PDF export.
|
|
26520
|
+
* The font will be loaded via the FontFace API for canvas rendering
|
|
26521
|
+
* and its raw bytes stored for PDF embedding.
|
|
26522
|
+
* @param options Font registration options (family + url or data)
|
|
26523
|
+
*/
|
|
26524
|
+
async registerFont(options) {
|
|
26525
|
+
Logger.log('[pc-editor] registerFont', options.family);
|
|
26526
|
+
await this.fontManager.registerFont(options);
|
|
26527
|
+
this.emit('font-registered', { family: options.family });
|
|
26528
|
+
// Re-render to pick up the new font if it's already in use
|
|
26529
|
+
if (this._isReady) {
|
|
26530
|
+
this.canvasManager.render();
|
|
26531
|
+
}
|
|
26532
|
+
}
|
|
26533
|
+
/**
|
|
26534
|
+
* Get all registered fonts (built-in and custom).
|
|
26535
|
+
*/
|
|
26536
|
+
getAvailableFonts() {
|
|
26537
|
+
return this.fontManager.getAvailableFonts().map(f => ({
|
|
26538
|
+
family: f.family,
|
|
26539
|
+
source: f.source
|
|
26540
|
+
}));
|
|
26541
|
+
}
|
|
26542
|
+
/**
|
|
26543
|
+
* Get all available font family names.
|
|
26544
|
+
*/
|
|
26545
|
+
getAvailableFontFamilies() {
|
|
26546
|
+
return this.fontManager.getAvailableFontFamilies();
|
|
26547
|
+
}
|
|
24677
26548
|
destroy() {
|
|
24678
26549
|
this.disableTextInput();
|
|
24679
26550
|
if (this.canvasManager) {
|
|
@@ -26190,7 +28061,7 @@ class MergeDataPane extends BasePane {
|
|
|
26190
28061
|
createContent() {
|
|
26191
28062
|
const container = document.createElement('div');
|
|
26192
28063
|
// Textarea for JSON
|
|
26193
|
-
const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
|
|
28064
|
+
const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
|
|
26194
28065
|
container.appendChild(textareaGroup);
|
|
26195
28066
|
// Error hint (hidden by default)
|
|
26196
28067
|
this.errorHint = this.createHint('');
|
|
@@ -26336,17 +28207,29 @@ class FormattingPane extends BasePane {
|
|
|
26336
28207
|
attach(options) {
|
|
26337
28208
|
super.attach(options);
|
|
26338
28209
|
if (this.editor) {
|
|
28210
|
+
// Populate font list from editor if no explicit list was provided
|
|
28211
|
+
if (this.fontFamilies === DEFAULT_FONT_FAMILIES) {
|
|
28212
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28213
|
+
this.rebuildFontSelect();
|
|
28214
|
+
}
|
|
26339
28215
|
// Update on cursor/selection changes
|
|
26340
28216
|
const updateHandler = () => this.updateFromEditor();
|
|
26341
28217
|
this.editor.on('cursor-changed', updateHandler);
|
|
26342
28218
|
this.editor.on('selection-changed', updateHandler);
|
|
26343
28219
|
this.editor.on('text-changed', updateHandler);
|
|
26344
28220
|
this.editor.on('formatting-changed', updateHandler);
|
|
28221
|
+
// Update font list when new fonts are registered
|
|
28222
|
+
const fontHandler = () => {
|
|
28223
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28224
|
+
this.rebuildFontSelect();
|
|
28225
|
+
};
|
|
28226
|
+
this.editor.on('font-registered', fontHandler);
|
|
26345
28227
|
this.eventCleanup.push(() => {
|
|
26346
28228
|
this.editor?.off('cursor-changed', updateHandler);
|
|
26347
28229
|
this.editor?.off('selection-changed', updateHandler);
|
|
26348
28230
|
this.editor?.off('text-changed', updateHandler);
|
|
26349
28231
|
this.editor?.off('formatting-changed', updateHandler);
|
|
28232
|
+
this.editor?.off('font-registered', fontHandler);
|
|
26350
28233
|
});
|
|
26351
28234
|
// Initial update
|
|
26352
28235
|
this.updateFromEditor();
|
|
@@ -26415,38 +28298,82 @@ class FormattingPane extends BasePane {
|
|
|
26415
28298
|
listsGroup.appendChild(this.outdentBtn);
|
|
26416
28299
|
listsSection.appendChild(listsGroup);
|
|
26417
28300
|
container.appendChild(listsSection);
|
|
26418
|
-
// Font section
|
|
28301
|
+
// Font section - label-value grid with right-aligned labels
|
|
26419
28302
|
const fontSection = this.createSection('Font');
|
|
28303
|
+
const fontGrid = document.createElement('div');
|
|
28304
|
+
fontGrid.className = 'pc-pane-label-value-grid';
|
|
28305
|
+
// Family row
|
|
28306
|
+
const familyLabel = document.createElement('label');
|
|
28307
|
+
familyLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28308
|
+
familyLabel.textContent = 'Family:';
|
|
26420
28309
|
this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
|
|
26421
28310
|
this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
|
|
26422
|
-
|
|
28311
|
+
fontGrid.appendChild(familyLabel);
|
|
28312
|
+
fontGrid.appendChild(this.fontFamilySelect);
|
|
28313
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28314
|
+
// Size row
|
|
28315
|
+
const sizeLabel = document.createElement('label');
|
|
28316
|
+
sizeLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28317
|
+
sizeLabel.textContent = 'Size:';
|
|
26423
28318
|
this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
|
|
26424
28319
|
this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
|
|
26425
|
-
|
|
28320
|
+
fontGrid.appendChild(sizeLabel);
|
|
28321
|
+
fontGrid.appendChild(this.fontSizeSelect);
|
|
28322
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28323
|
+
fontSection.appendChild(fontGrid);
|
|
26426
28324
|
container.appendChild(fontSection);
|
|
26427
|
-
// Color section
|
|
28325
|
+
// Color section - label-value grid with right-aligned labels
|
|
26428
28326
|
const colorSection = this.createSection('Color');
|
|
26429
|
-
const
|
|
26430
|
-
|
|
28327
|
+
const colorGrid = document.createElement('div');
|
|
28328
|
+
colorGrid.className = 'pc-pane-label-value-grid';
|
|
28329
|
+
// Text color row: label | picker | spacer
|
|
28330
|
+
const textColorLabel = document.createElement('label');
|
|
28331
|
+
textColorLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28332
|
+
textColorLabel.textContent = 'Text:';
|
|
26431
28333
|
this.colorInput = this.createColorInput('#000000');
|
|
26432
28334
|
this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
28335
|
+
colorGrid.appendChild(textColorLabel);
|
|
28336
|
+
colorGrid.appendChild(this.colorInput);
|
|
28337
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28338
|
+
// Highlight row: label | picker + clear button | spacer
|
|
28339
|
+
const highlightLabel = document.createElement('label');
|
|
28340
|
+
highlightLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28341
|
+
highlightLabel.textContent = 'Highlight:';
|
|
26436
28342
|
this.highlightInput = this.createColorInput('#ffff00');
|
|
26437
28343
|
this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
|
|
26438
|
-
const
|
|
28344
|
+
const highlightControls = document.createElement('div');
|
|
28345
|
+
highlightControls.style.display = 'flex';
|
|
28346
|
+
highlightControls.style.alignItems = 'center';
|
|
28347
|
+
highlightControls.style.gap = '4px';
|
|
28348
|
+
highlightControls.appendChild(this.highlightInput);
|
|
26439
28349
|
const clearHighlightBtn = this.createButton('Clear');
|
|
26440
28350
|
clearHighlightBtn.className = 'pc-pane-button';
|
|
26441
|
-
clearHighlightBtn.style.marginLeft = '4px';
|
|
26442
28351
|
this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
28352
|
+
highlightControls.appendChild(clearHighlightBtn);
|
|
28353
|
+
colorGrid.appendChild(highlightLabel);
|
|
28354
|
+
colorGrid.appendChild(highlightControls);
|
|
28355
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28356
|
+
colorSection.appendChild(colorGrid);
|
|
26447
28357
|
container.appendChild(colorSection);
|
|
26448
28358
|
return container;
|
|
26449
28359
|
}
|
|
28360
|
+
rebuildFontSelect() {
|
|
28361
|
+
if (!this.fontFamilySelect)
|
|
28362
|
+
return;
|
|
28363
|
+
const currentValue = this.fontFamilySelect.value;
|
|
28364
|
+
this.fontFamilySelect.innerHTML = '';
|
|
28365
|
+
for (const family of this.fontFamilies) {
|
|
28366
|
+
const option = document.createElement('option');
|
|
28367
|
+
option.value = family;
|
|
28368
|
+
option.textContent = family;
|
|
28369
|
+
option.style.fontFamily = family;
|
|
28370
|
+
this.fontFamilySelect.appendChild(option);
|
|
28371
|
+
}
|
|
28372
|
+
// Restore selection if the font still exists
|
|
28373
|
+
if (this.fontFamilies.includes(currentValue)) {
|
|
28374
|
+
this.fontFamilySelect.value = currentValue;
|
|
28375
|
+
}
|
|
28376
|
+
}
|
|
26450
28377
|
updateFromEditor() {
|
|
26451
28378
|
if (!this.editor)
|
|
26452
28379
|
return;
|
|
@@ -26500,9 +28427,15 @@ class FormattingPane extends BasePane {
|
|
|
26500
28427
|
this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
|
|
26501
28428
|
this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
|
|
26502
28429
|
}
|
|
28430
|
+
else {
|
|
28431
|
+
this.bulletListBtn?.classList.remove('pc-pane-button--active');
|
|
28432
|
+
this.numberedListBtn?.classList.remove('pc-pane-button--active');
|
|
28433
|
+
}
|
|
26503
28434
|
}
|
|
26504
28435
|
catch {
|
|
26505
28436
|
// No text editing active
|
|
28437
|
+
this.bulletListBtn?.classList.remove('pc-pane-button--active');
|
|
28438
|
+
this.numberedListBtn?.classList.remove('pc-pane-button--active');
|
|
26506
28439
|
}
|
|
26507
28440
|
}
|
|
26508
28441
|
getSelection() {
|
|
@@ -26664,10 +28597,10 @@ class HyperlinkPane extends BasePane {
|
|
|
26664
28597
|
const container = document.createElement('div');
|
|
26665
28598
|
// URL input
|
|
26666
28599
|
this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
|
|
26667
|
-
container.appendChild(this.createFormGroup('URL', this.urlInput));
|
|
28600
|
+
container.appendChild(this.createFormGroup('URL:', this.urlInput));
|
|
26668
28601
|
// Title input
|
|
26669
28602
|
this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
|
|
26670
|
-
container.appendChild(this.createFormGroup('Title', this.titleInput));
|
|
28603
|
+
container.appendChild(this.createFormGroup('Title:', this.titleInput));
|
|
26671
28604
|
// Apply button
|
|
26672
28605
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
26673
28606
|
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
@@ -26818,10 +28751,10 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26818
28751
|
const container = document.createElement('div');
|
|
26819
28752
|
// Field name input
|
|
26820
28753
|
this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
|
|
26821
|
-
container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
|
|
28754
|
+
container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
|
|
26822
28755
|
// Default value input
|
|
26823
28756
|
this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
|
|
26824
|
-
container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
|
|
28757
|
+
container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
|
|
26825
28758
|
// Value type select
|
|
26826
28759
|
this.valueTypeSelect = this.createSelect([
|
|
26827
28760
|
{ value: '', label: '(None)' },
|
|
@@ -26830,7 +28763,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26830
28763
|
{ value: 'date', label: 'Date' }
|
|
26831
28764
|
]);
|
|
26832
28765
|
this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
|
|
26833
|
-
container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
|
|
28766
|
+
container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
|
|
26834
28767
|
// Number format group
|
|
26835
28768
|
this.numberFormatGroup = this.createSection();
|
|
26836
28769
|
this.numberFormatGroup.style.display = 'none';
|
|
@@ -26840,7 +28773,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26840
28773
|
{ value: '0,0', label: 'Thousands separator (0,0)' },
|
|
26841
28774
|
{ value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
|
|
26842
28775
|
]);
|
|
26843
|
-
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
|
|
28776
|
+
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
|
|
26844
28777
|
container.appendChild(this.numberFormatGroup);
|
|
26845
28778
|
// Currency format group
|
|
26846
28779
|
this.currencyFormatGroup = this.createSection();
|
|
@@ -26851,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26851
28784
|
{ value: 'GBP', label: 'GBP' },
|
|
26852
28785
|
{ value: 'JPY', label: 'JPY' }
|
|
26853
28786
|
]);
|
|
26854
|
-
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
|
|
28787
|
+
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
|
|
26855
28788
|
container.appendChild(this.currencyFormatGroup);
|
|
26856
28789
|
// Date format group
|
|
26857
28790
|
this.dateFormatGroup = this.createSection();
|
|
@@ -26862,7 +28795,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26862
28795
|
{ value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
|
|
26863
28796
|
{ value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
|
|
26864
28797
|
]);
|
|
26865
|
-
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
|
|
28798
|
+
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
|
|
26866
28799
|
container.appendChild(this.dateFormatGroup);
|
|
26867
28800
|
// Apply button
|
|
26868
28801
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
@@ -27024,12 +28957,17 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27024
28957
|
if (this.editor) {
|
|
27025
28958
|
// Listen for repeating section selection
|
|
27026
28959
|
const selectionHandler = (event) => {
|
|
27027
|
-
|
|
27028
|
-
|
|
28960
|
+
const sel = event.selection || event;
|
|
28961
|
+
if (sel.type === 'repeating-section' && sel.sectionId) {
|
|
28962
|
+
const section = this.editor?.getRepeatingSection(sel.sectionId);
|
|
27029
28963
|
if (section) {
|
|
27030
28964
|
this.showSection(section);
|
|
27031
28965
|
}
|
|
27032
28966
|
}
|
|
28967
|
+
else {
|
|
28968
|
+
// Selection changed away from repeating section — hide pane
|
|
28969
|
+
this.hideSection();
|
|
28970
|
+
}
|
|
27033
28971
|
};
|
|
27034
28972
|
const removedHandler = () => {
|
|
27035
28973
|
this.hideSection();
|
|
@@ -27046,7 +28984,7 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27046
28984
|
const container = document.createElement('div');
|
|
27047
28985
|
// Field path input
|
|
27048
28986
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27049
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
28987
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27050
28988
|
hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
|
|
27051
28989
|
}));
|
|
27052
28990
|
// Apply button
|
|
@@ -27146,6 +29084,158 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27146
29084
|
}
|
|
27147
29085
|
}
|
|
27148
29086
|
|
|
29087
|
+
/**
|
|
29088
|
+
* ConditionalSectionPane - Edit conditional section properties.
|
|
29089
|
+
*
|
|
29090
|
+
* Shows:
|
|
29091
|
+
* - Predicate input (boolean expression in merge data)
|
|
29092
|
+
* - Position information
|
|
29093
|
+
*
|
|
29094
|
+
* Uses the PCEditor public API:
|
|
29095
|
+
* - editor.getConditionalSection()
|
|
29096
|
+
* - editor.updateConditionalSectionPredicate()
|
|
29097
|
+
* - editor.removeConditionalSection()
|
|
29098
|
+
*/
|
|
29099
|
+
class ConditionalSectionPane extends BasePane {
|
|
29100
|
+
constructor(id = 'conditional-section', options = {}) {
|
|
29101
|
+
super(id, { className: 'pc-pane-conditional-section', ...options });
|
|
29102
|
+
this.predicateInput = null;
|
|
29103
|
+
this.positionHint = null;
|
|
29104
|
+
this.currentSection = null;
|
|
29105
|
+
this.onApplyCallback = options.onApply;
|
|
29106
|
+
this.onRemoveCallback = options.onRemove;
|
|
29107
|
+
}
|
|
29108
|
+
attach(options) {
|
|
29109
|
+
super.attach(options);
|
|
29110
|
+
if (this.editor) {
|
|
29111
|
+
// Listen for conditional section selection
|
|
29112
|
+
const selectionHandler = (event) => {
|
|
29113
|
+
const sel = event.selection || event;
|
|
29114
|
+
if (sel.type === 'conditional-section' && sel.sectionId) {
|
|
29115
|
+
const section = this.editor?.getConditionalSection(sel.sectionId);
|
|
29116
|
+
if (section) {
|
|
29117
|
+
this.showSection(section);
|
|
29118
|
+
}
|
|
29119
|
+
}
|
|
29120
|
+
else {
|
|
29121
|
+
// Selection changed away from conditional section — hide pane
|
|
29122
|
+
this.hideSection();
|
|
29123
|
+
}
|
|
29124
|
+
};
|
|
29125
|
+
const removedHandler = () => {
|
|
29126
|
+
this.hideSection();
|
|
29127
|
+
};
|
|
29128
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29129
|
+
this.editor.on('conditional-section-removed', removedHandler);
|
|
29130
|
+
this.eventCleanup.push(() => {
|
|
29131
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29132
|
+
this.editor?.off('conditional-section-removed', removedHandler);
|
|
29133
|
+
});
|
|
29134
|
+
}
|
|
29135
|
+
}
|
|
29136
|
+
createContent() {
|
|
29137
|
+
const container = document.createElement('div');
|
|
29138
|
+
// Predicate input
|
|
29139
|
+
this.predicateInput = this.createTextInput({ placeholder: 'isActive' });
|
|
29140
|
+
container.appendChild(this.createFormGroup('Condition:', this.predicateInput, {
|
|
29141
|
+
hint: 'Boolean expression evaluated against merge data (e.g., "isActive", "count > 0")'
|
|
29142
|
+
}));
|
|
29143
|
+
// Apply button
|
|
29144
|
+
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
29145
|
+
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
29146
|
+
container.appendChild(applyBtn);
|
|
29147
|
+
// Remove button
|
|
29148
|
+
const removeBtn = this.createButton('Remove Condition', { variant: 'danger' });
|
|
29149
|
+
removeBtn.style.marginTop = '0.5rem';
|
|
29150
|
+
this.addButtonListener(removeBtn, () => this.removeSection());
|
|
29151
|
+
container.appendChild(removeBtn);
|
|
29152
|
+
// Position hint
|
|
29153
|
+
this.positionHint = this.createHint('');
|
|
29154
|
+
container.appendChild(this.positionHint);
|
|
29155
|
+
return container;
|
|
29156
|
+
}
|
|
29157
|
+
/**
|
|
29158
|
+
* Show the pane with the given section.
|
|
29159
|
+
*/
|
|
29160
|
+
showSection(section) {
|
|
29161
|
+
this.currentSection = section;
|
|
29162
|
+
if (this.predicateInput) {
|
|
29163
|
+
this.predicateInput.value = section.predicate;
|
|
29164
|
+
}
|
|
29165
|
+
if (this.positionHint) {
|
|
29166
|
+
this.positionHint.textContent = `Condition from position ${section.startIndex} to ${section.endIndex}`;
|
|
29167
|
+
}
|
|
29168
|
+
this.show();
|
|
29169
|
+
}
|
|
29170
|
+
/**
|
|
29171
|
+
* Hide the pane and clear the current section.
|
|
29172
|
+
*/
|
|
29173
|
+
hideSection() {
|
|
29174
|
+
this.currentSection = null;
|
|
29175
|
+
this.hide();
|
|
29176
|
+
}
|
|
29177
|
+
applyChanges() {
|
|
29178
|
+
if (!this.editor || !this.currentSection) {
|
|
29179
|
+
this.onApplyCallback?.(false, new Error('No section selected'));
|
|
29180
|
+
return;
|
|
29181
|
+
}
|
|
29182
|
+
const predicate = this.predicateInput?.value.trim();
|
|
29183
|
+
if (!predicate) {
|
|
29184
|
+
this.onApplyCallback?.(false, new Error('Predicate cannot be empty'));
|
|
29185
|
+
return;
|
|
29186
|
+
}
|
|
29187
|
+
if (predicate === this.currentSection.predicate) {
|
|
29188
|
+
return; // No changes
|
|
29189
|
+
}
|
|
29190
|
+
try {
|
|
29191
|
+
const success = this.editor.updateConditionalSectionPredicate(this.currentSection.id, predicate);
|
|
29192
|
+
if (success) {
|
|
29193
|
+
this.currentSection = this.editor.getConditionalSection(this.currentSection.id) || null;
|
|
29194
|
+
if (this.currentSection) {
|
|
29195
|
+
this.showSection(this.currentSection);
|
|
29196
|
+
}
|
|
29197
|
+
this.onApplyCallback?.(true);
|
|
29198
|
+
}
|
|
29199
|
+
else {
|
|
29200
|
+
this.onApplyCallback?.(false, new Error('Failed to update section'));
|
|
29201
|
+
}
|
|
29202
|
+
}
|
|
29203
|
+
catch (error) {
|
|
29204
|
+
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
29205
|
+
}
|
|
29206
|
+
}
|
|
29207
|
+
removeSection() {
|
|
29208
|
+
if (!this.editor || !this.currentSection)
|
|
29209
|
+
return;
|
|
29210
|
+
try {
|
|
29211
|
+
this.editor.removeConditionalSection(this.currentSection.id);
|
|
29212
|
+
this.hideSection();
|
|
29213
|
+
this.onRemoveCallback?.(true);
|
|
29214
|
+
}
|
|
29215
|
+
catch {
|
|
29216
|
+
this.onRemoveCallback?.(false);
|
|
29217
|
+
}
|
|
29218
|
+
}
|
|
29219
|
+
/**
|
|
29220
|
+
* Get the currently selected section.
|
|
29221
|
+
*/
|
|
29222
|
+
getCurrentSection() {
|
|
29223
|
+
return this.currentSection;
|
|
29224
|
+
}
|
|
29225
|
+
/**
|
|
29226
|
+
* Check if a section is currently selected.
|
|
29227
|
+
*/
|
|
29228
|
+
hasSection() {
|
|
29229
|
+
return this.currentSection !== null;
|
|
29230
|
+
}
|
|
29231
|
+
/**
|
|
29232
|
+
* Update the pane from current editor state.
|
|
29233
|
+
*/
|
|
29234
|
+
update() {
|
|
29235
|
+
// Section pane doesn't auto-update - it's driven by selection events
|
|
29236
|
+
}
|
|
29237
|
+
}
|
|
29238
|
+
|
|
27149
29239
|
/**
|
|
27150
29240
|
* TableRowLoopPane - Edit table row loop properties.
|
|
27151
29241
|
*
|
|
@@ -27170,14 +29260,28 @@ class TableRowLoopPane extends BasePane {
|
|
|
27170
29260
|
}
|
|
27171
29261
|
attach(options) {
|
|
27172
29262
|
super.attach(options);
|
|
27173
|
-
|
|
27174
|
-
|
|
29263
|
+
if (this.editor) {
|
|
29264
|
+
// Auto-show when a table row loop is clicked
|
|
29265
|
+
const loopClickHandler = (data) => {
|
|
29266
|
+
this.showLoop(data.table, data.loop);
|
|
29267
|
+
};
|
|
29268
|
+
// Hide when selection changes away from a loop
|
|
29269
|
+
const selectionHandler = () => {
|
|
29270
|
+
this.hideLoop();
|
|
29271
|
+
};
|
|
29272
|
+
this.editor.on('table-row-loop-clicked', loopClickHandler);
|
|
29273
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29274
|
+
this.eventCleanup.push(() => {
|
|
29275
|
+
this.editor?.off('table-row-loop-clicked', loopClickHandler);
|
|
29276
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29277
|
+
});
|
|
29278
|
+
}
|
|
27175
29279
|
}
|
|
27176
29280
|
createContent() {
|
|
27177
29281
|
const container = document.createElement('div');
|
|
27178
29282
|
// Field path input
|
|
27179
29283
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27180
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29284
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27181
29285
|
hint: 'Path to array in merge data (e.g., "items" or "orders")'
|
|
27182
29286
|
}));
|
|
27183
29287
|
// Apply button
|
|
@@ -27337,56 +29441,63 @@ class TextBoxPane extends BasePane {
|
|
|
27337
29441
|
}
|
|
27338
29442
|
createContent() {
|
|
27339
29443
|
const container = document.createElement('div');
|
|
27340
|
-
// Position section
|
|
29444
|
+
// Position section - Type on same row as label
|
|
27341
29445
|
const positionSection = this.createSection('Position');
|
|
27342
29446
|
this.positionSelect = this.createSelect([
|
|
27343
29447
|
{ value: 'inline', label: 'Inline' },
|
|
27344
29448
|
{ value: 'block', label: 'Block' },
|
|
27345
29449
|
{ value: 'relative', label: 'Relative' }
|
|
27346
29450
|
], 'inline');
|
|
27347
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27348
|
-
|
|
29451
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29452
|
+
this.updateOffsetVisibility();
|
|
29453
|
+
this.applyChanges();
|
|
29454
|
+
});
|
|
29455
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27349
29456
|
// Offset group (only visible for relative positioning)
|
|
27350
29457
|
this.offsetGroup = document.createElement('div');
|
|
27351
29458
|
this.offsetGroup.style.display = 'none';
|
|
27352
29459
|
const offsetRow = this.createRow();
|
|
27353
29460
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27354
29461
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27355
|
-
|
|
27356
|
-
|
|
29462
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29463
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29464
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29465
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27357
29466
|
this.offsetGroup.appendChild(offsetRow);
|
|
27358
29467
|
positionSection.appendChild(this.offsetGroup);
|
|
27359
29468
|
container.appendChild(positionSection);
|
|
27360
|
-
// Background
|
|
27361
|
-
const bgSection = this.createSection(
|
|
29469
|
+
// Background - color on same row as label
|
|
29470
|
+
const bgSection = this.createSection();
|
|
27362
29471
|
this.bgColorInput = this.createColorInput('#ffffff');
|
|
27363
|
-
|
|
29472
|
+
this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
|
|
29473
|
+
bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
|
|
27364
29474
|
container.appendChild(bgSection);
|
|
27365
29475
|
// Border section
|
|
27366
29476
|
const borderSection = this.createSection('Border');
|
|
27367
29477
|
const borderRow = this.createRow();
|
|
27368
29478
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
|
|
27369
29479
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27370
|
-
|
|
27371
|
-
|
|
29480
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyChanges());
|
|
29481
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyChanges());
|
|
29482
|
+
borderRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
29483
|
+
borderRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
27372
29484
|
borderSection.appendChild(borderRow);
|
|
29485
|
+
// Border style on same row as label
|
|
27373
29486
|
this.borderStyleSelect = this.createSelect([
|
|
27374
29487
|
{ value: 'solid', label: 'Solid' },
|
|
27375
29488
|
{ value: 'dashed', label: 'Dashed' },
|
|
27376
29489
|
{ value: 'dotted', label: 'Dotted' },
|
|
27377
29490
|
{ value: 'none', label: 'None' }
|
|
27378
29491
|
], 'solid');
|
|
27379
|
-
|
|
29492
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
|
|
29493
|
+
borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27380
29494
|
container.appendChild(borderSection);
|
|
27381
|
-
// Padding
|
|
27382
|
-
const paddingSection = this.createSection(
|
|
29495
|
+
// Padding on same row as label
|
|
29496
|
+
const paddingSection = this.createSection();
|
|
27383
29497
|
this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
|
|
27384
|
-
|
|
29498
|
+
this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
|
|
29499
|
+
paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
|
|
27385
29500
|
container.appendChild(paddingSection);
|
|
27386
|
-
// Apply button
|
|
27387
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27388
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27389
|
-
container.appendChild(applyBtn);
|
|
27390
29501
|
return container;
|
|
27391
29502
|
}
|
|
27392
29503
|
updateFromSelection() {
|
|
@@ -27462,7 +29573,6 @@ class TextBoxPane extends BasePane {
|
|
|
27462
29573
|
}
|
|
27463
29574
|
applyChanges() {
|
|
27464
29575
|
if (!this.editor || !this.currentTextBox) {
|
|
27465
|
-
this.onApplyCallback?.(false, new Error('No text box selected'));
|
|
27466
29576
|
return;
|
|
27467
29577
|
}
|
|
27468
29578
|
const updates = {};
|
|
@@ -27498,12 +29608,7 @@ class TextBoxPane extends BasePane {
|
|
|
27498
29608
|
}
|
|
27499
29609
|
try {
|
|
27500
29610
|
const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
|
|
27501
|
-
|
|
27502
|
-
this.onApplyCallback?.(true);
|
|
27503
|
-
}
|
|
27504
|
-
else {
|
|
27505
|
-
this.onApplyCallback?.(false, new Error('Failed to update text box'));
|
|
27506
|
-
}
|
|
29611
|
+
this.onApplyCallback?.(success);
|
|
27507
29612
|
}
|
|
27508
29613
|
catch (error) {
|
|
27509
29614
|
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -27579,28 +29684,35 @@ class ImagePane extends BasePane {
|
|
|
27579
29684
|
}
|
|
27580
29685
|
createContent() {
|
|
27581
29686
|
const container = document.createElement('div');
|
|
27582
|
-
// Position section
|
|
29687
|
+
// Position section — with heading, matching TextBoxPane
|
|
27583
29688
|
const positionSection = this.createSection('Position');
|
|
27584
29689
|
this.positionSelect = this.createSelect([
|
|
27585
29690
|
{ value: 'inline', label: 'Inline' },
|
|
27586
29691
|
{ value: 'block', label: 'Block' },
|
|
27587
29692
|
{ value: 'relative', label: 'Relative' }
|
|
27588
29693
|
], 'inline');
|
|
27589
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27590
|
-
|
|
29694
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29695
|
+
this.updateOffsetVisibility();
|
|
29696
|
+
this.applyChanges();
|
|
29697
|
+
});
|
|
29698
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27591
29699
|
// Offset group (only visible for relative positioning)
|
|
27592
29700
|
this.offsetGroup = document.createElement('div');
|
|
27593
29701
|
this.offsetGroup.style.display = 'none';
|
|
27594
29702
|
const offsetRow = this.createRow();
|
|
27595
29703
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27596
29704
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27597
|
-
|
|
27598
|
-
|
|
29705
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29706
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29707
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29708
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27599
29709
|
this.offsetGroup.appendChild(offsetRow);
|
|
27600
29710
|
positionSection.appendChild(this.offsetGroup);
|
|
27601
29711
|
container.appendChild(positionSection);
|
|
27602
|
-
|
|
27603
|
-
|
|
29712
|
+
container.appendChild(document.createElement('hr'));
|
|
29713
|
+
// Display section — Fit Mode and Resize Mode with aligned labels
|
|
29714
|
+
const displaySection = document.createElement('div');
|
|
29715
|
+
displaySection.className = 'pc-pane-image-display';
|
|
27604
29716
|
this.fitModeSelect = this.createSelect([
|
|
27605
29717
|
{ value: 'contain', label: 'Contain' },
|
|
27606
29718
|
{ value: 'cover', label: 'Cover' },
|
|
@@ -27608,34 +29720,31 @@ class ImagePane extends BasePane {
|
|
|
27608
29720
|
{ value: 'none', label: 'None (original size)' },
|
|
27609
29721
|
{ value: 'tile', label: 'Tile' }
|
|
27610
29722
|
], 'contain');
|
|
27611
|
-
|
|
29723
|
+
this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
|
|
29724
|
+
displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
|
|
27612
29725
|
this.resizeModeSelect = this.createSelect([
|
|
27613
29726
|
{ value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
|
|
27614
29727
|
{ value: 'free', label: 'Free Resize' }
|
|
27615
29728
|
], 'locked-aspect-ratio');
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
29729
|
+
this.addImmediateApplyListener(this.resizeModeSelect, () => this.applyChanges());
|
|
29730
|
+
displaySection.appendChild(this.createFormGroup('Resize Mode:', this.resizeModeSelect, { inline: true }));
|
|
29731
|
+
container.appendChild(displaySection);
|
|
29732
|
+
container.appendChild(document.createElement('hr'));
|
|
29733
|
+
// Alt Text — inline row
|
|
27620
29734
|
this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
|
|
27621
|
-
|
|
27622
|
-
container.appendChild(
|
|
27623
|
-
|
|
27624
|
-
|
|
29735
|
+
this.addImmediateApplyListener(this.altTextInput, () => this.applyChanges());
|
|
29736
|
+
container.appendChild(this.createFormGroup('Alt Text:', this.altTextInput, { inline: true }));
|
|
29737
|
+
container.appendChild(document.createElement('hr'));
|
|
29738
|
+
// Source — change image button
|
|
27625
29739
|
this.fileInput = document.createElement('input');
|
|
27626
29740
|
this.fileInput.type = 'file';
|
|
27627
29741
|
this.fileInput.accept = 'image/*';
|
|
27628
29742
|
this.fileInput.style.display = 'none';
|
|
27629
29743
|
this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
|
|
27630
|
-
|
|
29744
|
+
container.appendChild(this.fileInput);
|
|
27631
29745
|
const changeSourceBtn = this.createButton('Change Image...');
|
|
27632
29746
|
this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
|
|
27633
|
-
|
|
27634
|
-
container.appendChild(sourceSection);
|
|
27635
|
-
// Apply button
|
|
27636
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27637
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27638
|
-
container.appendChild(applyBtn);
|
|
29747
|
+
container.appendChild(changeSourceBtn);
|
|
27639
29748
|
return container;
|
|
27640
29749
|
}
|
|
27641
29750
|
updateFromSelection() {
|
|
@@ -27817,8 +29926,9 @@ class TablePane extends BasePane {
|
|
|
27817
29926
|
// Default controls
|
|
27818
29927
|
this.defaultPaddingInput = null;
|
|
27819
29928
|
this.defaultBorderColorInput = null;
|
|
27820
|
-
//
|
|
27821
|
-
this.
|
|
29929
|
+
// Merge/split buttons
|
|
29930
|
+
this.mergeCellsBtn = null;
|
|
29931
|
+
this.splitCellBtn = null;
|
|
27822
29932
|
// Cell formatting controls
|
|
27823
29933
|
this.cellBgColorInput = null;
|
|
27824
29934
|
this.borderTopCheck = null;
|
|
@@ -27838,12 +29948,12 @@ class TablePane extends BasePane {
|
|
|
27838
29948
|
// Listen for selection/focus changes
|
|
27839
29949
|
const updateHandler = () => this.updateFromFocusedTable();
|
|
27840
29950
|
this.editor.on('selection-change', updateHandler);
|
|
27841
|
-
this.editor.on('
|
|
27842
|
-
this.editor.on('table-cell-selection', updateHandler);
|
|
29951
|
+
this.editor.on('tablecell-cursor-changed', updateHandler);
|
|
29952
|
+
this.editor.on('table-cell-selection-changed', updateHandler);
|
|
27843
29953
|
this.eventCleanup.push(() => {
|
|
27844
29954
|
this.editor?.off('selection-change', updateHandler);
|
|
27845
|
-
this.editor?.off('
|
|
27846
|
-
this.editor?.off('table-cell-selection', updateHandler);
|
|
29955
|
+
this.editor?.off('tablecell-cursor-changed', updateHandler);
|
|
29956
|
+
this.editor?.off('table-cell-selection-changed', updateHandler);
|
|
27847
29957
|
});
|
|
27848
29958
|
// Initial update
|
|
27849
29959
|
this.updateFromFocusedTable();
|
|
@@ -27853,78 +29963,78 @@ class TablePane extends BasePane {
|
|
|
27853
29963
|
const container = document.createElement('div');
|
|
27854
29964
|
// Structure section
|
|
27855
29965
|
const structureSection = this.createSection('Structure');
|
|
29966
|
+
// Rows/Columns info with aligned labels
|
|
27856
29967
|
const structureInfo = document.createElement('div');
|
|
27857
|
-
structureInfo.className = 'pc-pane-info
|
|
29968
|
+
structureInfo.className = 'pc-pane-table-structure-info';
|
|
27858
29969
|
this.rowCountDisplay = document.createElement('span');
|
|
29970
|
+
this.rowCountDisplay.className = 'pc-pane-info-value';
|
|
27859
29971
|
this.colCountDisplay = document.createElement('span');
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
27863
|
-
rowInfo.appendChild(this.rowCountDisplay);
|
|
27864
|
-
const colInfo = document.createElement('div');
|
|
27865
|
-
colInfo.className = 'pc-pane-info';
|
|
27866
|
-
colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
|
|
27867
|
-
colInfo.appendChild(this.colCountDisplay);
|
|
27868
|
-
structureInfo.appendChild(rowInfo);
|
|
27869
|
-
structureInfo.appendChild(colInfo);
|
|
29972
|
+
this.colCountDisplay.className = 'pc-pane-info-value';
|
|
29973
|
+
structureInfo.appendChild(this.createFormGroup('Rows:', this.rowCountDisplay, { inline: true }));
|
|
29974
|
+
structureInfo.appendChild(this.createFormGroup('Columns:', this.colCountDisplay, { inline: true }));
|
|
27870
29975
|
structureSection.appendChild(structureInfo);
|
|
27871
|
-
// Row
|
|
27872
|
-
const
|
|
29976
|
+
// Row buttons
|
|
29977
|
+
const rowBtns = this.createButtonGroup();
|
|
27873
29978
|
const addRowBtn = this.createButton('+ Row');
|
|
27874
29979
|
this.addButtonListener(addRowBtn, () => this.insertRow());
|
|
27875
29980
|
const removeRowBtn = this.createButton('- Row');
|
|
27876
29981
|
this.addButtonListener(removeRowBtn, () => this.removeRow());
|
|
29982
|
+
rowBtns.appendChild(addRowBtn);
|
|
29983
|
+
rowBtns.appendChild(removeRowBtn);
|
|
29984
|
+
structureSection.appendChild(rowBtns);
|
|
29985
|
+
// Column buttons (separate row)
|
|
29986
|
+
const colBtns = this.createButtonGroup();
|
|
27877
29987
|
const addColBtn = this.createButton('+ Column');
|
|
27878
29988
|
this.addButtonListener(addColBtn, () => this.insertColumn());
|
|
27879
29989
|
const removeColBtn = this.createButton('- Column');
|
|
27880
29990
|
this.addButtonListener(removeColBtn, () => this.removeColumn());
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
|
|
27885
|
-
structureSection.appendChild(
|
|
27886
|
-
|
|
27887
|
-
|
|
27888
|
-
const headersSection = this.createSection('Headers');
|
|
27889
|
-
const headerRow = this.createRow();
|
|
29991
|
+
colBtns.appendChild(addColBtn);
|
|
29992
|
+
colBtns.appendChild(removeColBtn);
|
|
29993
|
+
structureSection.appendChild(colBtns);
|
|
29994
|
+
// Header rows/cols (with separator and aligned labels)
|
|
29995
|
+
structureSection.appendChild(document.createElement('hr'));
|
|
29996
|
+
const headersGroup = document.createElement('div');
|
|
29997
|
+
headersGroup.className = 'pc-pane-table-headers';
|
|
27890
29998
|
this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
29999
|
+
this.addImmediateApplyListener(this.headerRowInput, () => this.applyHeaders());
|
|
30000
|
+
headersGroup.appendChild(this.createFormGroup('Header Rows:', this.headerRowInput, { inline: true }));
|
|
27891
30001
|
this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
27892
|
-
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
|
|
27896
|
-
|
|
27897
|
-
headersSection.appendChild(applyHeadersBtn);
|
|
27898
|
-
container.appendChild(headersSection);
|
|
27899
|
-
// Row Loop section
|
|
27900
|
-
const loopSection = this.createSection('Row Loop');
|
|
27901
|
-
this.loopFieldInput = this.createTextInput({ placeholder: 'items' });
|
|
27902
|
-
loopSection.appendChild(this.createFormGroup('Array Field', this.loopFieldInput, {
|
|
27903
|
-
hint: 'Creates a loop on the currently focused row'
|
|
27904
|
-
}));
|
|
27905
|
-
const createLoopBtn = this.createButton('Create Row Loop');
|
|
27906
|
-
this.addButtonListener(createLoopBtn, () => this.createRowLoop());
|
|
27907
|
-
loopSection.appendChild(createLoopBtn);
|
|
27908
|
-
container.appendChild(loopSection);
|
|
27909
|
-
// Defaults section
|
|
30002
|
+
this.addImmediateApplyListener(this.headerColInput, () => this.applyHeaders());
|
|
30003
|
+
headersGroup.appendChild(this.createFormGroup('Header Cols:', this.headerColInput, { inline: true }));
|
|
30004
|
+
structureSection.appendChild(headersGroup);
|
|
30005
|
+
container.appendChild(structureSection);
|
|
30006
|
+
// Defaults section (aligned labels)
|
|
27910
30007
|
const defaultsSection = this.createSection('Defaults');
|
|
27911
|
-
const
|
|
30008
|
+
const defaultsGroup = document.createElement('div');
|
|
30009
|
+
defaultsGroup.className = 'pc-pane-table-defaults';
|
|
27912
30010
|
this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
|
|
30011
|
+
this.addImmediateApplyListener(this.defaultPaddingInput, () => this.applyDefaults());
|
|
30012
|
+
defaultsGroup.appendChild(this.createFormGroup('Padding:', this.defaultPaddingInput, { inline: true }));
|
|
27913
30013
|
this.defaultBorderColorInput = this.createColorInput('#cccccc');
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
defaultsSection.appendChild(
|
|
27917
|
-
const applyDefaultsBtn = this.createButton('Apply Defaults');
|
|
27918
|
-
this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
|
|
27919
|
-
defaultsSection.appendChild(applyDefaultsBtn);
|
|
30014
|
+
this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
|
|
30015
|
+
defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
|
|
30016
|
+
defaultsSection.appendChild(defaultsGroup);
|
|
27920
30017
|
container.appendChild(defaultsSection);
|
|
27921
30018
|
// Cell formatting section
|
|
27922
30019
|
const cellSection = this.createSection('Cell Formatting');
|
|
27923
30020
|
this.cellSelectionDisplay = this.createHint('No cell selected');
|
|
27924
30021
|
cellSection.appendChild(this.cellSelectionDisplay);
|
|
27925
|
-
//
|
|
30022
|
+
// Merge/Split buttons
|
|
30023
|
+
const mergeBtnGroup = this.createButtonGroup();
|
|
30024
|
+
this.mergeCellsBtn = this.createButton('Merge Cells');
|
|
30025
|
+
this.mergeCellsBtn.disabled = true;
|
|
30026
|
+
this.splitCellBtn = this.createButton('Split Cell');
|
|
30027
|
+
this.splitCellBtn.disabled = true;
|
|
30028
|
+
this.addButtonListener(this.mergeCellsBtn, () => this.doMergeCells());
|
|
30029
|
+
this.addButtonListener(this.splitCellBtn, () => this.doSplitCell());
|
|
30030
|
+
mergeBtnGroup.appendChild(this.mergeCellsBtn);
|
|
30031
|
+
mergeBtnGroup.appendChild(this.splitCellBtn);
|
|
30032
|
+
cellSection.appendChild(mergeBtnGroup);
|
|
30033
|
+
cellSection.appendChild(document.createElement('hr'));
|
|
30034
|
+
// Background — inline
|
|
27926
30035
|
this.cellBgColorInput = this.createColorInput('#ffffff');
|
|
27927
|
-
|
|
30036
|
+
this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
|
|
30037
|
+
cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
|
|
27928
30038
|
// Border checkboxes
|
|
27929
30039
|
const borderChecks = document.createElement('div');
|
|
27930
30040
|
borderChecks.className = 'pc-pane-row';
|
|
@@ -27956,24 +30066,29 @@ class TablePane extends BasePane {
|
|
|
27956
30066
|
checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
|
|
27957
30067
|
if (checkLabels[3])
|
|
27958
30068
|
checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
|
|
27959
|
-
|
|
30069
|
+
// Add change listeners for immediate apply on checkboxes
|
|
30070
|
+
for (const check of [this.borderTopCheck, this.borderRightCheck, this.borderBottomCheck, this.borderLeftCheck]) {
|
|
30071
|
+
check.addEventListener('change', () => this.applyCellFormatting());
|
|
30072
|
+
}
|
|
30073
|
+
cellSection.appendChild(this.createFormGroup('Borders:', borderChecks));
|
|
27960
30074
|
// Border properties
|
|
27961
30075
|
const borderPropsRow = this.createRow();
|
|
27962
30076
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
|
|
27963
30077
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27964
|
-
|
|
27965
|
-
|
|
30078
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyCellFormatting());
|
|
30079
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyCellFormatting());
|
|
30080
|
+
borderPropsRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
30081
|
+
borderPropsRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
27966
30082
|
cellSection.appendChild(borderPropsRow);
|
|
30083
|
+
// Style — inline
|
|
27967
30084
|
this.borderStyleSelect = this.createSelect([
|
|
27968
30085
|
{ value: 'solid', label: 'Solid' },
|
|
27969
30086
|
{ value: 'dashed', label: 'Dashed' },
|
|
27970
30087
|
{ value: 'dotted', label: 'Dotted' },
|
|
27971
30088
|
{ value: 'none', label: 'None' }
|
|
27972
30089
|
], 'solid');
|
|
27973
|
-
|
|
27974
|
-
|
|
27975
|
-
this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
|
|
27976
|
-
cellSection.appendChild(applyCellBtn);
|
|
30090
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
|
|
30091
|
+
cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27977
30092
|
container.appendChild(cellSection);
|
|
27978
30093
|
return container;
|
|
27979
30094
|
}
|
|
@@ -28038,6 +30153,15 @@ class TablePane extends BasePane {
|
|
|
28038
30153
|
return;
|
|
28039
30154
|
const focusedCell = table.focusedCell;
|
|
28040
30155
|
const selectedRange = table.selectedRange;
|
|
30156
|
+
// Update merge/split button states
|
|
30157
|
+
if (this.mergeCellsBtn) {
|
|
30158
|
+
const canMerge = selectedRange ? table.canMergeRange(selectedRange).canMerge : false;
|
|
30159
|
+
this.mergeCellsBtn.disabled = !canMerge;
|
|
30160
|
+
}
|
|
30161
|
+
if (this.splitCellBtn) {
|
|
30162
|
+
const canSplit = focusedCell ? table.canSplitCell(focusedCell.row, focusedCell.col).canSplit : false;
|
|
30163
|
+
this.splitCellBtn.disabled = !canSplit;
|
|
30164
|
+
}
|
|
28041
30165
|
if (selectedRange) {
|
|
28042
30166
|
const count = (selectedRange.end.row - selectedRange.start.row + 1) *
|
|
28043
30167
|
(selectedRange.end.col - selectedRange.start.col + 1);
|
|
@@ -28195,20 +30319,20 @@ class TablePane extends BasePane {
|
|
|
28195
30319
|
hasTable() {
|
|
28196
30320
|
return this.currentTable !== null;
|
|
28197
30321
|
}
|
|
28198
|
-
|
|
28199
|
-
if (!this.editor || !this.currentTable)
|
|
28200
|
-
this.onApplyCallback?.(false, new Error('No table focused'));
|
|
30322
|
+
doMergeCells() {
|
|
30323
|
+
if (!this.editor || !this.currentTable)
|
|
28201
30324
|
return;
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
30325
|
+
this.editor.tableMergeCells(this.currentTable);
|
|
30326
|
+
this.updateFromFocusedTable();
|
|
30327
|
+
}
|
|
30328
|
+
doSplitCell() {
|
|
30329
|
+
if (!this.editor || !this.currentTable)
|
|
28206
30330
|
return;
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
this.editor.
|
|
28211
|
-
this.
|
|
30331
|
+
const focused = this.currentTable.focusedCell;
|
|
30332
|
+
if (!focused)
|
|
30333
|
+
return;
|
|
30334
|
+
this.editor.tableSplitCell(this.currentTable, focused.row, focused.col);
|
|
30335
|
+
this.updateFromFocusedTable();
|
|
28212
30336
|
}
|
|
28213
30337
|
/**
|
|
28214
30338
|
* Update the pane from current editor state.
|
|
@@ -28218,5 +30342,5 @@ class TablePane extends BasePane {
|
|
|
28218
30342
|
}
|
|
28219
30343
|
}
|
|
28220
30344
|
|
|
28221
|
-
export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, Logger, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
|
|
30345
|
+
export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ConditionalSectionManager, ConditionalSectionPane, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FontManager, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, Logger, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, PredicateEvaluator, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
|
|
28222
30346
|
//# sourceMappingURL=pc-editor.esm.js.map
|