@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.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var pdfLib = require('pdf-lib');
|
|
4
|
+
var fontkit = require('@pdf-lib/fontkit');
|
|
4
5
|
var pdfjsLib = require('pdfjs-dist');
|
|
5
6
|
|
|
6
7
|
function _interopNamespaceDefault(e) {
|
|
@@ -2307,6 +2308,291 @@ class RepeatingSectionManager extends EventEmitter {
|
|
|
2307
2308
|
}
|
|
2308
2309
|
}
|
|
2309
2310
|
|
|
2311
|
+
/**
|
|
2312
|
+
* Manages conditional sections within text content.
|
|
2313
|
+
* Conditional sections define ranges of content that are shown or hidden
|
|
2314
|
+
* based on a boolean predicate evaluated against merge data.
|
|
2315
|
+
* They start and end at paragraph boundaries.
|
|
2316
|
+
*/
|
|
2317
|
+
class ConditionalSectionManager extends EventEmitter {
|
|
2318
|
+
constructor() {
|
|
2319
|
+
super();
|
|
2320
|
+
this.sections = new Map();
|
|
2321
|
+
this.nextId = 1;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Create a new conditional section.
|
|
2325
|
+
* @param startIndex Text index at paragraph start (must be 0 or immediately after a newline)
|
|
2326
|
+
* @param endIndex Text index at closing paragraph start (must be immediately after a newline)
|
|
2327
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
2328
|
+
*/
|
|
2329
|
+
create(startIndex, endIndex, predicate) {
|
|
2330
|
+
const id = `cond-${this.nextId++}`;
|
|
2331
|
+
const section = {
|
|
2332
|
+
id,
|
|
2333
|
+
predicate,
|
|
2334
|
+
startIndex,
|
|
2335
|
+
endIndex
|
|
2336
|
+
};
|
|
2337
|
+
this.sections.set(id, section);
|
|
2338
|
+
this.emit('section-added', { section });
|
|
2339
|
+
return section;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Remove a conditional section by ID.
|
|
2343
|
+
*/
|
|
2344
|
+
remove(id) {
|
|
2345
|
+
const section = this.sections.get(id);
|
|
2346
|
+
if (section) {
|
|
2347
|
+
this.sections.delete(id);
|
|
2348
|
+
this.emit('section-removed', { section });
|
|
2349
|
+
}
|
|
2350
|
+
return section;
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Get a conditional section by ID.
|
|
2354
|
+
*/
|
|
2355
|
+
getSection(id) {
|
|
2356
|
+
return this.sections.get(id);
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Get all conditional sections.
|
|
2360
|
+
*/
|
|
2361
|
+
getSections() {
|
|
2362
|
+
return Array.from(this.sections.values());
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Get all conditional sections sorted by startIndex.
|
|
2366
|
+
*/
|
|
2367
|
+
getSectionsSorted() {
|
|
2368
|
+
return this.getSections().sort((a, b) => a.startIndex - b.startIndex);
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Get all conditional sections sorted by startIndex in descending order.
|
|
2372
|
+
* Useful for processing sections end-to-start during merge.
|
|
2373
|
+
*/
|
|
2374
|
+
getSectionsDescending() {
|
|
2375
|
+
return this.getSections().sort((a, b) => b.startIndex - a.startIndex);
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Find a conditional section that contains the given text index.
|
|
2379
|
+
*/
|
|
2380
|
+
getSectionContaining(textIndex) {
|
|
2381
|
+
for (const section of this.sections.values()) {
|
|
2382
|
+
if (textIndex >= section.startIndex && textIndex < section.endIndex) {
|
|
2383
|
+
return section;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return undefined;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
2390
|
+
* Returns the section if textIndex matches startIndex or endIndex.
|
|
2391
|
+
*/
|
|
2392
|
+
getSectionAtBoundary(textIndex) {
|
|
2393
|
+
for (const section of this.sections.values()) {
|
|
2394
|
+
if (section.startIndex === textIndex || section.endIndex === textIndex) {
|
|
2395
|
+
return section;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return undefined;
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Update a section's predicate.
|
|
2402
|
+
*/
|
|
2403
|
+
updatePredicate(id, predicate) {
|
|
2404
|
+
const section = this.sections.get(id);
|
|
2405
|
+
if (!section) {
|
|
2406
|
+
return false;
|
|
2407
|
+
}
|
|
2408
|
+
section.predicate = predicate;
|
|
2409
|
+
this.emit('section-updated', { section });
|
|
2410
|
+
return true;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Update a section's visual state (called during rendering).
|
|
2414
|
+
*/
|
|
2415
|
+
updateVisualState(id, visualState) {
|
|
2416
|
+
const section = this.sections.get(id);
|
|
2417
|
+
if (!section) {
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
section.visualState = visualState;
|
|
2421
|
+
return true;
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Shift section positions when text is inserted.
|
|
2425
|
+
* @param fromIndex The position where text was inserted
|
|
2426
|
+
* @param delta The number of characters inserted (positive)
|
|
2427
|
+
*/
|
|
2428
|
+
shiftSections(fromIndex, delta) {
|
|
2429
|
+
let changed = false;
|
|
2430
|
+
for (const section of this.sections.values()) {
|
|
2431
|
+
if (fromIndex <= section.startIndex) {
|
|
2432
|
+
section.startIndex += delta;
|
|
2433
|
+
section.endIndex += delta;
|
|
2434
|
+
changed = true;
|
|
2435
|
+
}
|
|
2436
|
+
else if (fromIndex < section.endIndex) {
|
|
2437
|
+
section.endIndex += delta;
|
|
2438
|
+
changed = true;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (changed) {
|
|
2442
|
+
this.emit('sections-shifted', { fromIndex, delta });
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Handle deletion of text range.
|
|
2447
|
+
* Sections entirely within the deleted range are removed.
|
|
2448
|
+
* Sections partially overlapping are adjusted or removed.
|
|
2449
|
+
* @returns Array of removed sections
|
|
2450
|
+
*/
|
|
2451
|
+
handleDeletion(start, length) {
|
|
2452
|
+
const end = start + length;
|
|
2453
|
+
const removedSections = [];
|
|
2454
|
+
const sectionsToUpdate = [];
|
|
2455
|
+
for (const section of this.sections.values()) {
|
|
2456
|
+
if (section.startIndex >= start && section.endIndex <= end) {
|
|
2457
|
+
removedSections.push(section);
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
if (section.startIndex < end && section.endIndex > start) {
|
|
2461
|
+
if (start <= section.startIndex) {
|
|
2462
|
+
removedSections.push(section);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (start < section.endIndex) {
|
|
2466
|
+
if (end >= section.endIndex) {
|
|
2467
|
+
const newEnd = start;
|
|
2468
|
+
if (newEnd <= section.startIndex) {
|
|
2469
|
+
removedSections.push(section);
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
sectionsToUpdate.push({
|
|
2473
|
+
id: section.id,
|
|
2474
|
+
newStart: section.startIndex,
|
|
2475
|
+
newEnd: newEnd
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
else {
|
|
2479
|
+
const newEnd = section.endIndex - length;
|
|
2480
|
+
sectionsToUpdate.push({
|
|
2481
|
+
id: section.id,
|
|
2482
|
+
newStart: section.startIndex,
|
|
2483
|
+
newEnd: newEnd
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (section.startIndex >= end) {
|
|
2490
|
+
sectionsToUpdate.push({
|
|
2491
|
+
id: section.id,
|
|
2492
|
+
newStart: section.startIndex - length,
|
|
2493
|
+
newEnd: section.endIndex - length
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
for (const section of removedSections) {
|
|
2498
|
+
this.sections.delete(section.id);
|
|
2499
|
+
this.emit('section-removed', { section });
|
|
2500
|
+
}
|
|
2501
|
+
for (const update of sectionsToUpdate) {
|
|
2502
|
+
const section = this.sections.get(update.id);
|
|
2503
|
+
if (section) {
|
|
2504
|
+
section.startIndex = update.newStart;
|
|
2505
|
+
section.endIndex = update.newEnd;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (removedSections.length > 0 || sectionsToUpdate.length > 0) {
|
|
2509
|
+
this.emit('sections-changed');
|
|
2510
|
+
}
|
|
2511
|
+
return removedSections;
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Validate that the given boundaries are at paragraph boundaries.
|
|
2515
|
+
* Also checks that conditionals don't partially overlap repeating sections.
|
|
2516
|
+
* @param start The proposed start index
|
|
2517
|
+
* @param end The proposed end index
|
|
2518
|
+
* @param content The text content to validate against
|
|
2519
|
+
* @returns true if valid, false otherwise
|
|
2520
|
+
*/
|
|
2521
|
+
validateBoundaries(start, end, content) {
|
|
2522
|
+
if (start !== 0 && content[start - 1] !== '\n') {
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2525
|
+
if (end !== 0 && end < content.length && content[end - 1] !== '\n') {
|
|
2526
|
+
return false;
|
|
2527
|
+
}
|
|
2528
|
+
if (end <= start) {
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
// Check for overlapping conditional sections
|
|
2532
|
+
for (const existing of this.sections.values()) {
|
|
2533
|
+
if ((start >= existing.startIndex && start < existing.endIndex) ||
|
|
2534
|
+
(end > existing.startIndex && end <= existing.endIndex) ||
|
|
2535
|
+
(start <= existing.startIndex && end >= existing.endIndex)) {
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Get the number of conditional sections.
|
|
2543
|
+
*/
|
|
2544
|
+
get count() {
|
|
2545
|
+
return this.sections.size;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Check if there are any conditional sections.
|
|
2549
|
+
*/
|
|
2550
|
+
get isEmpty() {
|
|
2551
|
+
return this.sections.size === 0;
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Clear all conditional sections.
|
|
2555
|
+
*/
|
|
2556
|
+
clear() {
|
|
2557
|
+
const hadSections = this.sections.size > 0;
|
|
2558
|
+
this.sections.clear();
|
|
2559
|
+
if (hadSections) {
|
|
2560
|
+
this.emit('sections-cleared');
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Serialize all sections to JSON.
|
|
2565
|
+
*/
|
|
2566
|
+
toJSON() {
|
|
2567
|
+
return this.getSectionsSorted().map(section => ({
|
|
2568
|
+
id: section.id,
|
|
2569
|
+
predicate: section.predicate,
|
|
2570
|
+
startIndex: section.startIndex,
|
|
2571
|
+
endIndex: section.endIndex
|
|
2572
|
+
}));
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Load sections from serialized data.
|
|
2576
|
+
*/
|
|
2577
|
+
fromJSON(data) {
|
|
2578
|
+
this.clear();
|
|
2579
|
+
for (const sectionData of data) {
|
|
2580
|
+
const section = {
|
|
2581
|
+
id: sectionData.id,
|
|
2582
|
+
predicate: sectionData.predicate,
|
|
2583
|
+
startIndex: sectionData.startIndex,
|
|
2584
|
+
endIndex: sectionData.endIndex
|
|
2585
|
+
};
|
|
2586
|
+
this.sections.set(section.id, section);
|
|
2587
|
+
const idNum = parseInt(sectionData.id.replace('cond-', ''), 10);
|
|
2588
|
+
if (!isNaN(idNum) && idNum >= this.nextId) {
|
|
2589
|
+
this.nextId = idNum + 1;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
this.emit('sections-loaded', { count: this.sections.size });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2310
2596
|
/**
|
|
2311
2597
|
* HyperlinkManager - Manages hyperlinks within flowing text content
|
|
2312
2598
|
*/
|
|
@@ -6212,12 +6498,20 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6212
6498
|
this._coveredCells = new Map();
|
|
6213
6499
|
// Row loops for merge expansion
|
|
6214
6500
|
this._rowLoops = new Map();
|
|
6501
|
+
// Row conditionals for conditional display
|
|
6502
|
+
this._rowConditionals = new Map();
|
|
6215
6503
|
// Layout caching for performance
|
|
6216
6504
|
this._layoutDirty = true;
|
|
6217
6505
|
this._cachedRowHeights = [];
|
|
6218
6506
|
this._cachedRowPositions = [];
|
|
6219
6507
|
// Multi-page rendering info: pageIndex -> slice render info
|
|
6220
6508
|
this._renderedSlices = new Map();
|
|
6509
|
+
// ============================================
|
|
6510
|
+
// Table Row Conditionals
|
|
6511
|
+
// ============================================
|
|
6512
|
+
this._nextCondId = 1;
|
|
6513
|
+
this._selectedRowLoopId = null;
|
|
6514
|
+
this._selectedRowConditionalId = null;
|
|
6221
6515
|
// Tables ONLY support block positioning - force it regardless of config
|
|
6222
6516
|
this._position = 'block';
|
|
6223
6517
|
// Initialize defaults
|
|
@@ -6911,6 +7205,85 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6911
7205
|
loopRangesOverlap(start1, end1, start2, end2) {
|
|
6912
7206
|
return start1 <= end2 && start2 <= end1;
|
|
6913
7207
|
}
|
|
7208
|
+
generateRowConditionalId() {
|
|
7209
|
+
return `row-cond-${this._nextCondId++}`;
|
|
7210
|
+
}
|
|
7211
|
+
/**
|
|
7212
|
+
* Create a row conditional.
|
|
7213
|
+
*/
|
|
7214
|
+
createRowConditional(startRowIndex, endRowIndex, predicate) {
|
|
7215
|
+
if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
|
|
7216
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Invalid row range');
|
|
7217
|
+
return null;
|
|
7218
|
+
}
|
|
7219
|
+
if (startRowIndex > endRowIndex) {
|
|
7220
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Start index must be <= end index');
|
|
7221
|
+
return null;
|
|
7222
|
+
}
|
|
7223
|
+
// Check for overlapping conditionals
|
|
7224
|
+
for (const existing of this._rowConditionals.values()) {
|
|
7225
|
+
if (this.loopRangesOverlap(startRowIndex, endRowIndex, existing.startRowIndex, existing.endRowIndex)) {
|
|
7226
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Range overlaps with existing conditional');
|
|
7227
|
+
return null;
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
const cond = {
|
|
7231
|
+
id: this.generateRowConditionalId(),
|
|
7232
|
+
predicate,
|
|
7233
|
+
startRowIndex,
|
|
7234
|
+
endRowIndex
|
|
7235
|
+
};
|
|
7236
|
+
this._rowConditionals.set(cond.id, cond);
|
|
7237
|
+
this.emit('row-conditional-created', { conditional: cond });
|
|
7238
|
+
this.emit('content-changed', {});
|
|
7239
|
+
return cond;
|
|
7240
|
+
}
|
|
7241
|
+
/**
|
|
7242
|
+
* Remove a row conditional by ID.
|
|
7243
|
+
*/
|
|
7244
|
+
removeRowConditional(id) {
|
|
7245
|
+
const cond = this._rowConditionals.get(id);
|
|
7246
|
+
if (!cond)
|
|
7247
|
+
return false;
|
|
7248
|
+
this._rowConditionals.delete(id);
|
|
7249
|
+
this.emit('row-conditional-removed', { conditionalId: id });
|
|
7250
|
+
return true;
|
|
7251
|
+
}
|
|
7252
|
+
/**
|
|
7253
|
+
* Get a row conditional by ID.
|
|
7254
|
+
*/
|
|
7255
|
+
getRowConditional(id) {
|
|
7256
|
+
return this._rowConditionals.get(id);
|
|
7257
|
+
}
|
|
7258
|
+
/**
|
|
7259
|
+
* Get all row conditionals.
|
|
7260
|
+
*/
|
|
7261
|
+
getAllRowConditionals() {
|
|
7262
|
+
return Array.from(this._rowConditionals.values());
|
|
7263
|
+
}
|
|
7264
|
+
/**
|
|
7265
|
+
* Get the row conditional at a given row index.
|
|
7266
|
+
*/
|
|
7267
|
+
getRowConditionalAtRow(rowIndex) {
|
|
7268
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7269
|
+
if (rowIndex >= cond.startRowIndex && rowIndex <= cond.endRowIndex) {
|
|
7270
|
+
return cond;
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
return undefined;
|
|
7274
|
+
}
|
|
7275
|
+
/**
|
|
7276
|
+
* Update a row conditional's predicate.
|
|
7277
|
+
*/
|
|
7278
|
+
updateRowConditionalPredicate(id, predicate) {
|
|
7279
|
+
const cond = this._rowConditionals.get(id);
|
|
7280
|
+
if (!cond)
|
|
7281
|
+
return false;
|
|
7282
|
+
cond.predicate = predicate;
|
|
7283
|
+
this.emit('row-conditional-updated', { conditional: cond });
|
|
7284
|
+
this.emit('content-changed', {});
|
|
7285
|
+
return true;
|
|
7286
|
+
}
|
|
6914
7287
|
/**
|
|
6915
7288
|
* Shift row loop indices when rows are inserted or removed.
|
|
6916
7289
|
* @param fromIndex The row index where insertion/removal occurred
|
|
@@ -6951,6 +7324,41 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6951
7324
|
this._rowLoops.delete(id);
|
|
6952
7325
|
this.emit('row-loop-removed', { loopId: id, reason: 'row-deleted' });
|
|
6953
7326
|
}
|
|
7327
|
+
// Also shift row conditional indices
|
|
7328
|
+
this.shiftRowConditionalIndices(fromIndex, delta);
|
|
7329
|
+
}
|
|
7330
|
+
/**
|
|
7331
|
+
* Shift row conditional indices when rows are inserted or removed.
|
|
7332
|
+
*/
|
|
7333
|
+
shiftRowConditionalIndices(fromIndex, delta) {
|
|
7334
|
+
const condsToRemove = [];
|
|
7335
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7336
|
+
if (delta < 0) {
|
|
7337
|
+
const removeCount = Math.abs(delta);
|
|
7338
|
+
const removeEnd = fromIndex + removeCount - 1;
|
|
7339
|
+
if (fromIndex <= cond.endRowIndex && removeEnd >= cond.startRowIndex) {
|
|
7340
|
+
condsToRemove.push(cond.id);
|
|
7341
|
+
continue;
|
|
7342
|
+
}
|
|
7343
|
+
if (fromIndex < cond.startRowIndex) {
|
|
7344
|
+
cond.startRowIndex += delta;
|
|
7345
|
+
cond.endRowIndex += delta;
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
else {
|
|
7349
|
+
if (fromIndex <= cond.startRowIndex) {
|
|
7350
|
+
cond.startRowIndex += delta;
|
|
7351
|
+
cond.endRowIndex += delta;
|
|
7352
|
+
}
|
|
7353
|
+
else if (fromIndex <= cond.endRowIndex) {
|
|
7354
|
+
cond.endRowIndex += delta;
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
for (const id of condsToRemove) {
|
|
7359
|
+
this._rowConditionals.delete(id);
|
|
7360
|
+
this.emit('row-conditional-removed', { conditionalId: id, reason: 'row-deleted' });
|
|
7361
|
+
}
|
|
6954
7362
|
}
|
|
6955
7363
|
/**
|
|
6956
7364
|
* Get rows in a range (for loop expansion).
|
|
@@ -7758,6 +8166,9 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7758
8166
|
result.mergedCell.markReflowDirty();
|
|
7759
8167
|
}
|
|
7760
8168
|
this.clearSelection();
|
|
8169
|
+
// Focus the anchor (top-left) cell of the merged range
|
|
8170
|
+
const normalized = TableCellMerger.normalizeRange(mergeRange);
|
|
8171
|
+
this.focusCell(normalized.start.row, normalized.start.col);
|
|
7761
8172
|
this.emit('cells-merged', { range: mergeRange });
|
|
7762
8173
|
this.emit('content-changed', {});
|
|
7763
8174
|
}
|
|
@@ -7848,6 +8259,10 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7848
8259
|
if (this._rowLoops.size > 0) {
|
|
7849
8260
|
this.renderRowLoopIndicators(ctx);
|
|
7850
8261
|
}
|
|
8262
|
+
// Render row conditional indicators
|
|
8263
|
+
if (this._rowConditionals.size > 0) {
|
|
8264
|
+
this.renderRowConditionalIndicators(ctx);
|
|
8265
|
+
}
|
|
7851
8266
|
// Render cell range selection highlight
|
|
7852
8267
|
if (this._selectedRange) {
|
|
7853
8268
|
this.renderRangeSelection(ctx);
|
|
@@ -7862,11 +8277,54 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7862
8277
|
}
|
|
7863
8278
|
}
|
|
7864
8279
|
/**
|
|
7865
|
-
*
|
|
8280
|
+
* Select a row loop by ID (for pane display).
|
|
7866
8281
|
*/
|
|
8282
|
+
selectRowLoop(loopId) {
|
|
8283
|
+
this._selectedRowLoopId = loopId;
|
|
8284
|
+
}
|
|
8285
|
+
/**
|
|
8286
|
+
* Get the currently selected row loop ID.
|
|
8287
|
+
*/
|
|
8288
|
+
get selectedRowLoopId() {
|
|
8289
|
+
return this._selectedRowLoopId;
|
|
8290
|
+
}
|
|
8291
|
+
/**
|
|
8292
|
+
* Hit-test a point against row loop labels.
|
|
8293
|
+
* Point should be in table-local coordinates.
|
|
8294
|
+
* Returns the loop if a label was clicked, null otherwise.
|
|
8295
|
+
*/
|
|
8296
|
+
getRowLoopAtPoint(point) {
|
|
8297
|
+
let rowPositions = this._cachedRowPositions;
|
|
8298
|
+
if (rowPositions.length === 0) {
|
|
8299
|
+
rowPositions = [];
|
|
8300
|
+
let y = 0;
|
|
8301
|
+
for (const row of this._rows) {
|
|
8302
|
+
rowPositions.push(y);
|
|
8303
|
+
y += row.calculatedHeight;
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
for (const loop of this._rowLoops.values()) {
|
|
8307
|
+
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
8308
|
+
let _endY = startY;
|
|
8309
|
+
for (let i = loop.startRowIndex; i <= loop.endRowIndex && i < this._rows.length; i++) {
|
|
8310
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8311
|
+
}
|
|
8312
|
+
// Label bounds (matches rendering)
|
|
8313
|
+
const labelWidth = 30; // approximate for "Loop" at 10px
|
|
8314
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8315
|
+
const labelX = -6 - labelWidth - 4;
|
|
8316
|
+
const labelY = startY - labelHeight - 2;
|
|
8317
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8318
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8319
|
+
return loop;
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
return null;
|
|
8323
|
+
}
|
|
7867
8324
|
renderRowLoopIndicators(ctx) {
|
|
7868
|
-
const
|
|
7869
|
-
const
|
|
8325
|
+
const color = TableObject.LOOP_COLOR;
|
|
8326
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8327
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
7870
8328
|
// Calculate row Y positions if not cached
|
|
7871
8329
|
let rowPositions = this._cachedRowPositions;
|
|
7872
8330
|
if (rowPositions.length === 0) {
|
|
@@ -7877,12 +8335,8 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7877
8335
|
y += row.calculatedHeight;
|
|
7878
8336
|
}
|
|
7879
8337
|
}
|
|
7880
|
-
// Colors for different loops (cycle through these)
|
|
7881
|
-
const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
|
|
7882
|
-
let colorIndex = 0;
|
|
7883
8338
|
for (const loop of this._rowLoops.values()) {
|
|
7884
|
-
const
|
|
7885
|
-
colorIndex++;
|
|
8339
|
+
const isSelected = this._selectedRowLoopId === loop.id;
|
|
7886
8340
|
// Calculate the Y range for this loop
|
|
7887
8341
|
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
7888
8342
|
let endY = startY;
|
|
@@ -7892,31 +8346,149 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7892
8346
|
const loopHeight = endY - startY;
|
|
7893
8347
|
// Draw colored stripe on left side
|
|
7894
8348
|
ctx.fillStyle = color;
|
|
7895
|
-
ctx.fillRect(-
|
|
7896
|
-
// Draw
|
|
8349
|
+
ctx.fillRect(-6, startY, 4, loopHeight);
|
|
8350
|
+
// Draw vertical connector line
|
|
8351
|
+
ctx.strokeStyle = color;
|
|
8352
|
+
ctx.lineWidth = 1;
|
|
8353
|
+
ctx.beginPath();
|
|
8354
|
+
ctx.moveTo(-4, startY);
|
|
8355
|
+
ctx.lineTo(-4, endY);
|
|
8356
|
+
ctx.stroke();
|
|
8357
|
+
// Draw "Loop" label — matches text flow style
|
|
7897
8358
|
ctx.save();
|
|
7898
8359
|
ctx.font = '10px Arial';
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
const
|
|
7902
|
-
const
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
ctx.
|
|
8360
|
+
const labelText = 'Loop';
|
|
8361
|
+
const metrics = ctx.measureText(labelText);
|
|
8362
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8363
|
+
const boxHeight = 10 + padding * 2;
|
|
8364
|
+
const labelX = -6 - boxWidth - 4;
|
|
8365
|
+
const labelY = startY - boxHeight - 2;
|
|
8366
|
+
ctx.beginPath();
|
|
8367
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8368
|
+
if (isSelected) {
|
|
8369
|
+
// Selected: filled background with white text
|
|
8370
|
+
ctx.fillStyle = color;
|
|
8371
|
+
ctx.fill();
|
|
8372
|
+
ctx.fillStyle = '#ffffff';
|
|
8373
|
+
}
|
|
8374
|
+
else {
|
|
8375
|
+
// Not selected: white background, outlined with colored text
|
|
8376
|
+
ctx.fillStyle = '#ffffff';
|
|
8377
|
+
ctx.fill();
|
|
8378
|
+
ctx.strokeStyle = color;
|
|
8379
|
+
ctx.lineWidth = 1.5;
|
|
8380
|
+
ctx.stroke();
|
|
8381
|
+
ctx.fillStyle = color;
|
|
8382
|
+
}
|
|
8383
|
+
ctx.textBaseline = 'middle';
|
|
8384
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
7906
8385
|
ctx.restore();
|
|
7907
|
-
|
|
8386
|
+
}
|
|
8387
|
+
}
|
|
8388
|
+
/**
|
|
8389
|
+
* Select a row conditional by ID (for pane display).
|
|
8390
|
+
*/
|
|
8391
|
+
selectRowConditional(conditionalId) {
|
|
8392
|
+
this._selectedRowConditionalId = conditionalId;
|
|
8393
|
+
}
|
|
8394
|
+
/**
|
|
8395
|
+
* Get the currently selected row conditional ID.
|
|
8396
|
+
*/
|
|
8397
|
+
get selectedRowConditionalId() {
|
|
8398
|
+
return this._selectedRowConditionalId;
|
|
8399
|
+
}
|
|
8400
|
+
/**
|
|
8401
|
+
* Hit-test a point against row conditional labels.
|
|
8402
|
+
* Point should be in table-local coordinates.
|
|
8403
|
+
*/
|
|
8404
|
+
getRowConditionalAtPoint(point) {
|
|
8405
|
+
let rowPositions = this._cachedRowPositions;
|
|
8406
|
+
if (rowPositions.length === 0) {
|
|
8407
|
+
rowPositions = [];
|
|
8408
|
+
let y = 0;
|
|
8409
|
+
for (const row of this._rows) {
|
|
8410
|
+
rowPositions.push(y);
|
|
8411
|
+
y += row.calculatedHeight;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8415
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8416
|
+
let _endY = startY;
|
|
8417
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8418
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8419
|
+
}
|
|
8420
|
+
// Label bounds (right side of table, offset from loop labels)
|
|
8421
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8422
|
+
const labelWidth = 22; // approximate for "If" at 10px
|
|
8423
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8424
|
+
const labelX = totalWidth + 10;
|
|
8425
|
+
const labelY = startY - labelHeight - 2;
|
|
8426
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8427
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8428
|
+
return cond;
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8431
|
+
return null;
|
|
8432
|
+
}
|
|
8433
|
+
renderRowConditionalIndicators(ctx) {
|
|
8434
|
+
const color = TableObject.COND_COLOR;
|
|
8435
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8436
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
8437
|
+
let rowPositions = this._cachedRowPositions;
|
|
8438
|
+
if (rowPositions.length === 0) {
|
|
8439
|
+
rowPositions = [];
|
|
8440
|
+
let y = 0;
|
|
8441
|
+
for (const row of this._rows) {
|
|
8442
|
+
rowPositions.push(y);
|
|
8443
|
+
y += row.calculatedHeight;
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8447
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8448
|
+
const isSelected = this._selectedRowConditionalId === cond.id;
|
|
8449
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8450
|
+
let endY = startY;
|
|
8451
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8452
|
+
endY += this._rows[i].calculatedHeight;
|
|
8453
|
+
}
|
|
8454
|
+
const condHeight = endY - startY;
|
|
8455
|
+
// Draw colored stripe on right side
|
|
8456
|
+
ctx.fillStyle = color;
|
|
8457
|
+
ctx.fillRect(totalWidth + 2, startY, 4, condHeight);
|
|
8458
|
+
// Draw vertical connector line
|
|
7908
8459
|
ctx.strokeStyle = color;
|
|
7909
8460
|
ctx.lineWidth = 1;
|
|
7910
8461
|
ctx.beginPath();
|
|
7911
|
-
|
|
7912
|
-
ctx.
|
|
7913
|
-
ctx.lineTo(-indicatorWidth - 6, startY);
|
|
7914
|
-
ctx.lineTo(-indicatorWidth - 6, startY + 6);
|
|
7915
|
-
// Bottom bracket
|
|
7916
|
-
ctx.moveTo(-indicatorWidth - 2, endY);
|
|
7917
|
-
ctx.lineTo(-indicatorWidth - 6, endY);
|
|
7918
|
-
ctx.lineTo(-indicatorWidth - 6, endY - 6);
|
|
8462
|
+
ctx.moveTo(totalWidth + 4, startY);
|
|
8463
|
+
ctx.lineTo(totalWidth + 4, endY);
|
|
7919
8464
|
ctx.stroke();
|
|
8465
|
+
// Draw "If" label
|
|
8466
|
+
ctx.save();
|
|
8467
|
+
ctx.font = '10px Arial';
|
|
8468
|
+
const labelText = 'If';
|
|
8469
|
+
const metrics = ctx.measureText(labelText);
|
|
8470
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8471
|
+
const boxHeight = 10 + padding * 2;
|
|
8472
|
+
const labelX = totalWidth + 10;
|
|
8473
|
+
const labelY = startY - boxHeight - 2;
|
|
8474
|
+
ctx.beginPath();
|
|
8475
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8476
|
+
if (isSelected) {
|
|
8477
|
+
ctx.fillStyle = color;
|
|
8478
|
+
ctx.fill();
|
|
8479
|
+
ctx.fillStyle = '#ffffff';
|
|
8480
|
+
}
|
|
8481
|
+
else {
|
|
8482
|
+
ctx.fillStyle = '#ffffff';
|
|
8483
|
+
ctx.fill();
|
|
8484
|
+
ctx.strokeStyle = color;
|
|
8485
|
+
ctx.lineWidth = 1.5;
|
|
8486
|
+
ctx.stroke();
|
|
8487
|
+
ctx.fillStyle = color;
|
|
8488
|
+
}
|
|
8489
|
+
ctx.textBaseline = 'middle';
|
|
8490
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
8491
|
+
ctx.restore();
|
|
7920
8492
|
}
|
|
7921
8493
|
}
|
|
7922
8494
|
/**
|
|
@@ -8035,6 +8607,14 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8035
8607
|
columns: this._columns.map(col => ({ ...col })),
|
|
8036
8608
|
rows: this._rows.map(row => row.toData()),
|
|
8037
8609
|
rowLoops,
|
|
8610
|
+
rowConditionals: this._rowConditionals.size > 0
|
|
8611
|
+
? Array.from(this._rowConditionals.values()).map(c => ({
|
|
8612
|
+
id: c.id,
|
|
8613
|
+
predicate: c.predicate,
|
|
8614
|
+
startRowIndex: c.startRowIndex,
|
|
8615
|
+
endRowIndex: c.endRowIndex
|
|
8616
|
+
}))
|
|
8617
|
+
: undefined,
|
|
8038
8618
|
defaultCellPadding: this._defaultCellPadding,
|
|
8039
8619
|
defaultBorderColor: this._defaultBorderColor,
|
|
8040
8620
|
defaultBorderWidth: this._defaultBorderWidth,
|
|
@@ -8078,6 +8658,17 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8078
8658
|
});
|
|
8079
8659
|
}
|
|
8080
8660
|
}
|
|
8661
|
+
// Load row conditionals if present
|
|
8662
|
+
if (data.data.rowConditionals) {
|
|
8663
|
+
for (const condData of data.data.rowConditionals) {
|
|
8664
|
+
table._rowConditionals.set(condData.id, {
|
|
8665
|
+
id: condData.id,
|
|
8666
|
+
predicate: condData.predicate,
|
|
8667
|
+
startRowIndex: condData.startRowIndex,
|
|
8668
|
+
endRowIndex: condData.endRowIndex
|
|
8669
|
+
});
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8081
8672
|
table.updateCoveredCells();
|
|
8082
8673
|
return table;
|
|
8083
8674
|
}
|
|
@@ -8107,6 +8698,18 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8107
8698
|
});
|
|
8108
8699
|
}
|
|
8109
8700
|
}
|
|
8701
|
+
// Restore row conditionals if any
|
|
8702
|
+
this._rowConditionals.clear();
|
|
8703
|
+
if (data.data.rowConditionals) {
|
|
8704
|
+
for (const condData of data.data.rowConditionals) {
|
|
8705
|
+
this._rowConditionals.set(condData.id, {
|
|
8706
|
+
id: condData.id,
|
|
8707
|
+
predicate: condData.predicate,
|
|
8708
|
+
startRowIndex: condData.startRowIndex,
|
|
8709
|
+
endRowIndex: condData.endRowIndex
|
|
8710
|
+
});
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8110
8713
|
// Restore defaults
|
|
8111
8714
|
if (data.data.defaultCellPadding !== undefined) {
|
|
8112
8715
|
this._defaultCellPadding = data.data.defaultCellPadding;
|
|
@@ -8126,6 +8729,13 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8126
8729
|
return TableObject.fromData(this.toData());
|
|
8127
8730
|
}
|
|
8128
8731
|
}
|
|
8732
|
+
/**
|
|
8733
|
+
* Render row loop indicators (colored stripe on left side of loop rows).
|
|
8734
|
+
*/
|
|
8735
|
+
TableObject.LOOP_COLOR = '#6B46C1';
|
|
8736
|
+
TableObject.LOOP_LABEL_PADDING = 4;
|
|
8737
|
+
TableObject.LOOP_LABEL_RADIUS = 4;
|
|
8738
|
+
TableObject.COND_COLOR = '#D97706'; // Orange
|
|
8129
8739
|
|
|
8130
8740
|
/**
|
|
8131
8741
|
* TableResizeHandler - Handles column and row resize operations for tables.
|
|
@@ -8511,6 +9121,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8511
9121
|
this.substitutionFields = new SubstitutionFieldManager();
|
|
8512
9122
|
this.embeddedObjects = new EmbeddedObjectManager();
|
|
8513
9123
|
this.repeatingSections = new RepeatingSectionManager();
|
|
9124
|
+
this.conditionalSections = new ConditionalSectionManager();
|
|
8514
9125
|
this.hyperlinks = new HyperlinkManager();
|
|
8515
9126
|
this.layout = new TextLayout();
|
|
8516
9127
|
this.setupEventForwarding();
|
|
@@ -8548,6 +9159,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8548
9159
|
this.substitutionFields.handleDeletion(data.start, data.length);
|
|
8549
9160
|
this.embeddedObjects.handleDeletion(data.start, data.length);
|
|
8550
9161
|
this.repeatingSections.handleDeletion(data.start, data.length);
|
|
9162
|
+
this.conditionalSections.handleDeletion(data.start, data.length);
|
|
8551
9163
|
this.paragraphFormatting.handleDeletion(data.start, data.length);
|
|
8552
9164
|
this.hyperlinks.handleDeletion(data.start, data.length);
|
|
8553
9165
|
this.emit('content-changed', {
|
|
@@ -8599,6 +9211,16 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8599
9211
|
this.repeatingSections.on('section-updated', (data) => {
|
|
8600
9212
|
this.emit('repeating-section-updated', data);
|
|
8601
9213
|
});
|
|
9214
|
+
// Forward conditional section events
|
|
9215
|
+
this.conditionalSections.on('section-added', (data) => {
|
|
9216
|
+
this.emit('conditional-section-added', data);
|
|
9217
|
+
});
|
|
9218
|
+
this.conditionalSections.on('section-removed', (data) => {
|
|
9219
|
+
this.emit('conditional-section-removed', data);
|
|
9220
|
+
});
|
|
9221
|
+
this.conditionalSections.on('section-updated', (data) => {
|
|
9222
|
+
this.emit('conditional-section-updated', data);
|
|
9223
|
+
});
|
|
8602
9224
|
// Forward hyperlink events
|
|
8603
9225
|
this.hyperlinks.on('hyperlink-added', (data) => {
|
|
8604
9226
|
this.emit('hyperlink-added', data);
|
|
@@ -8656,6 +9278,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8656
9278
|
this.substitutionFields.shiftFields(insertAt, text.length);
|
|
8657
9279
|
this.embeddedObjects.shiftObjects(insertAt, text.length);
|
|
8658
9280
|
this.repeatingSections.shiftSections(insertAt, text.length);
|
|
9281
|
+
this.conditionalSections.shiftSections(insertAt, text.length);
|
|
8659
9282
|
this.hyperlinks.shiftHyperlinks(insertAt, text.length);
|
|
8660
9283
|
// Insert the text first so we have the full content
|
|
8661
9284
|
this.textState.insertText(text, insertAt);
|
|
@@ -8728,6 +9351,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8728
9351
|
this.substitutionFields.shiftFields(position, text.length);
|
|
8729
9352
|
this.embeddedObjects.shiftObjects(position, text.length);
|
|
8730
9353
|
this.repeatingSections.shiftSections(position, text.length);
|
|
9354
|
+
this.conditionalSections.shiftSections(position, text.length);
|
|
8731
9355
|
this.hyperlinks.shiftHyperlinks(position, text.length);
|
|
8732
9356
|
// Insert the text
|
|
8733
9357
|
const content = this.textState.getText();
|
|
@@ -8745,6 +9369,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8745
9369
|
this.substitutionFields.handleDeletion(position, length);
|
|
8746
9370
|
this.embeddedObjects.handleDeletion(position, length);
|
|
8747
9371
|
this.repeatingSections.handleDeletion(position, length);
|
|
9372
|
+
this.conditionalSections.handleDeletion(position, length);
|
|
8748
9373
|
this.paragraphFormatting.handleDeletion(position, length);
|
|
8749
9374
|
this.hyperlinks.handleDeletion(position, length);
|
|
8750
9375
|
// Delete the text
|
|
@@ -9134,6 +9759,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9134
9759
|
this.substitutionFields.shiftFields(insertAt, 1);
|
|
9135
9760
|
this.embeddedObjects.shiftObjects(insertAt, 1);
|
|
9136
9761
|
this.repeatingSections.shiftSections(insertAt, 1);
|
|
9762
|
+
this.conditionalSections.shiftSections(insertAt, 1);
|
|
9137
9763
|
// Insert the placeholder character
|
|
9138
9764
|
this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
|
|
9139
9765
|
// Shift paragraph formatting with the complete content
|
|
@@ -9381,6 +10007,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9381
10007
|
this.substitutionFields.clear();
|
|
9382
10008
|
this.embeddedObjects.clear();
|
|
9383
10009
|
this.repeatingSections.clear();
|
|
10010
|
+
this.conditionalSections.clear();
|
|
9384
10011
|
this.hyperlinks.clear();
|
|
9385
10012
|
}
|
|
9386
10013
|
// ============================================
|
|
@@ -9737,44 +10364,60 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9737
10364
|
// List Operations
|
|
9738
10365
|
// ============================================
|
|
9739
10366
|
/**
|
|
9740
|
-
*
|
|
10367
|
+
* Get paragraph starts affected by the current selection or cursor position.
|
|
9741
10368
|
*/
|
|
9742
|
-
|
|
10369
|
+
getAffectedParagraphStarts() {
|
|
10370
|
+
const content = this.textState.getText();
|
|
10371
|
+
const selection = this.getSelection();
|
|
10372
|
+
if (selection && selection.start !== selection.end) {
|
|
10373
|
+
return this.paragraphFormatting.getParagraphBoundariesInRange(selection.start, selection.end, content);
|
|
10374
|
+
}
|
|
9743
10375
|
const cursorPos = this.textState.getCursorPosition();
|
|
10376
|
+
return [this.paragraphFormatting.getParagraphStart(cursorPos, content)];
|
|
10377
|
+
}
|
|
10378
|
+
/**
|
|
10379
|
+
* Toggle bullet list for the current paragraph(s) in selection.
|
|
10380
|
+
*/
|
|
10381
|
+
toggleBulletList() {
|
|
9744
10382
|
const content = this.textState.getText();
|
|
9745
|
-
const
|
|
9746
|
-
|
|
9747
|
-
|
|
10383
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10384
|
+
for (const start of paragraphStarts) {
|
|
10385
|
+
this.paragraphFormatting.toggleList(start, 'bullet');
|
|
10386
|
+
}
|
|
10387
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9748
10388
|
}
|
|
9749
10389
|
/**
|
|
9750
|
-
* Toggle numbered list for the current paragraph
|
|
10390
|
+
* Toggle numbered list for the current paragraph(s) in selection.
|
|
9751
10391
|
*/
|
|
9752
10392
|
toggleNumberedList() {
|
|
9753
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9754
10393
|
const content = this.textState.getText();
|
|
9755
|
-
const
|
|
9756
|
-
|
|
9757
|
-
|
|
10394
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10395
|
+
for (const start of paragraphStarts) {
|
|
10396
|
+
this.paragraphFormatting.toggleList(start, 'number');
|
|
10397
|
+
}
|
|
10398
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9758
10399
|
}
|
|
9759
10400
|
/**
|
|
9760
|
-
* Indent the current paragraph
|
|
10401
|
+
* Indent the current paragraph(s) in selection.
|
|
9761
10402
|
*/
|
|
9762
10403
|
indentParagraph() {
|
|
9763
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9764
10404
|
const content = this.textState.getText();
|
|
9765
|
-
const
|
|
9766
|
-
|
|
9767
|
-
|
|
10405
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10406
|
+
for (const start of paragraphStarts) {
|
|
10407
|
+
this.paragraphFormatting.indentParagraph(start);
|
|
10408
|
+
}
|
|
10409
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9768
10410
|
}
|
|
9769
10411
|
/**
|
|
9770
|
-
* Outdent the current paragraph
|
|
10412
|
+
* Outdent the current paragraph(s) in selection.
|
|
9771
10413
|
*/
|
|
9772
10414
|
outdentParagraph() {
|
|
9773
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9774
10415
|
const content = this.textState.getText();
|
|
9775
|
-
const
|
|
9776
|
-
|
|
9777
|
-
|
|
10416
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10417
|
+
for (const start of paragraphStarts) {
|
|
10418
|
+
this.paragraphFormatting.outdentParagraph(start);
|
|
10419
|
+
}
|
|
10420
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9778
10421
|
}
|
|
9779
10422
|
/**
|
|
9780
10423
|
* Get the list formatting for the current paragraph.
|
|
@@ -9936,6 +10579,79 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9936
10579
|
return result;
|
|
9937
10580
|
}
|
|
9938
10581
|
// ============================================
|
|
10582
|
+
// Conditional Section Operations
|
|
10583
|
+
// ============================================
|
|
10584
|
+
/**
|
|
10585
|
+
* Get the conditional section manager.
|
|
10586
|
+
*/
|
|
10587
|
+
getConditionalSectionManager() {
|
|
10588
|
+
return this.conditionalSections;
|
|
10589
|
+
}
|
|
10590
|
+
/**
|
|
10591
|
+
* Get all conditional sections.
|
|
10592
|
+
*/
|
|
10593
|
+
getConditionalSections() {
|
|
10594
|
+
return this.conditionalSections.getSections();
|
|
10595
|
+
}
|
|
10596
|
+
/**
|
|
10597
|
+
* Create a conditional section.
|
|
10598
|
+
* @param startIndex Text index at paragraph start (must be at a paragraph boundary)
|
|
10599
|
+
* @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
|
|
10600
|
+
* @param predicate The predicate expression to evaluate
|
|
10601
|
+
* @returns The created section, or null if boundaries are invalid
|
|
10602
|
+
*/
|
|
10603
|
+
createConditionalSection(startIndex, endIndex, predicate) {
|
|
10604
|
+
const content = this.textState.getText();
|
|
10605
|
+
if (!this.conditionalSections.validateBoundaries(startIndex, endIndex, content)) {
|
|
10606
|
+
return null;
|
|
10607
|
+
}
|
|
10608
|
+
const section = this.conditionalSections.create(startIndex, endIndex, predicate);
|
|
10609
|
+
this.emit('content-changed', {
|
|
10610
|
+
text: content,
|
|
10611
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10612
|
+
});
|
|
10613
|
+
return section;
|
|
10614
|
+
}
|
|
10615
|
+
/**
|
|
10616
|
+
* Remove a conditional section by ID.
|
|
10617
|
+
*/
|
|
10618
|
+
removeConditionalSection(id) {
|
|
10619
|
+
const section = this.conditionalSections.remove(id);
|
|
10620
|
+
if (section) {
|
|
10621
|
+
this.emit('content-changed', {
|
|
10622
|
+
text: this.textState.getText(),
|
|
10623
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10624
|
+
});
|
|
10625
|
+
return true;
|
|
10626
|
+
}
|
|
10627
|
+
return false;
|
|
10628
|
+
}
|
|
10629
|
+
/**
|
|
10630
|
+
* Get a conditional section by ID.
|
|
10631
|
+
*/
|
|
10632
|
+
getConditionalSection(id) {
|
|
10633
|
+
return this.conditionalSections.getSection(id);
|
|
10634
|
+
}
|
|
10635
|
+
/**
|
|
10636
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
10637
|
+
*/
|
|
10638
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
10639
|
+
return this.conditionalSections.getSectionAtBoundary(textIndex);
|
|
10640
|
+
}
|
|
10641
|
+
/**
|
|
10642
|
+
* Update a conditional section's predicate.
|
|
10643
|
+
*/
|
|
10644
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
10645
|
+
const result = this.conditionalSections.updatePredicate(id, predicate);
|
|
10646
|
+
if (result) {
|
|
10647
|
+
this.emit('content-changed', {
|
|
10648
|
+
text: this.textState.getText(),
|
|
10649
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10650
|
+
});
|
|
10651
|
+
}
|
|
10652
|
+
return result;
|
|
10653
|
+
}
|
|
10654
|
+
// ============================================
|
|
9939
10655
|
// Serialization
|
|
9940
10656
|
// ============================================
|
|
9941
10657
|
/**
|
|
@@ -9989,6 +10705,8 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9989
10705
|
}));
|
|
9990
10706
|
// Serialize repeating sections
|
|
9991
10707
|
const repeatingSectionsData = this.repeatingSections.toJSON();
|
|
10708
|
+
// Serialize conditional sections
|
|
10709
|
+
const conditionalSectionsData = this.conditionalSections.toJSON();
|
|
9992
10710
|
// Serialize embedded objects
|
|
9993
10711
|
const embeddedObjects = [];
|
|
9994
10712
|
const objectsMap = this.embeddedObjects.getObjects();
|
|
@@ -10006,6 +10724,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10006
10724
|
paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
|
|
10007
10725
|
substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
|
|
10008
10726
|
repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
|
|
10727
|
+
conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
|
|
10009
10728
|
embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
|
|
10010
10729
|
hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
|
|
10011
10730
|
};
|
|
@@ -10044,6 +10763,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10044
10763
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10045
10764
|
content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
|
|
10046
10765
|
}
|
|
10766
|
+
// Restore conditional sections
|
|
10767
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10768
|
+
content.getConditionalSectionManager().fromJSON(data.conditionalSections);
|
|
10769
|
+
}
|
|
10047
10770
|
// Restore embedded objects using factory
|
|
10048
10771
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10049
10772
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10098,6 +10821,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10098
10821
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10099
10822
|
this.repeatingSections.fromJSON(data.repeatingSections);
|
|
10100
10823
|
}
|
|
10824
|
+
// Restore conditional sections
|
|
10825
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10826
|
+
this.conditionalSections.fromJSON(data.conditionalSections);
|
|
10827
|
+
}
|
|
10101
10828
|
// Restore embedded objects
|
|
10102
10829
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10103
10830
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10117,6 +10844,349 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10117
10844
|
}
|
|
10118
10845
|
}
|
|
10119
10846
|
|
|
10847
|
+
/**
|
|
10848
|
+
* Simple recursive-descent predicate evaluator.
|
|
10849
|
+
* Supports:
|
|
10850
|
+
* - Truthiness: `isActive`
|
|
10851
|
+
* - Negation: `!isActive`
|
|
10852
|
+
* - Comparisons: ==, !=, >, <, >=, <=
|
|
10853
|
+
* - Logical: &&, ||, parentheses
|
|
10854
|
+
* - Literals: "approved", 100, true/false
|
|
10855
|
+
* - Dot notation: customer.isVIP
|
|
10856
|
+
*/
|
|
10857
|
+
function tokenize(input) {
|
|
10858
|
+
const tokens = [];
|
|
10859
|
+
let i = 0;
|
|
10860
|
+
while (i < input.length) {
|
|
10861
|
+
const ch = input[i];
|
|
10862
|
+
// Skip whitespace
|
|
10863
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
10864
|
+
i++;
|
|
10865
|
+
continue;
|
|
10866
|
+
}
|
|
10867
|
+
// Parentheses
|
|
10868
|
+
if (ch === '(' || ch === ')') {
|
|
10869
|
+
tokens.push({ type: 'paren', value: ch });
|
|
10870
|
+
i++;
|
|
10871
|
+
continue;
|
|
10872
|
+
}
|
|
10873
|
+
// Two-character operators
|
|
10874
|
+
if (i + 1 < input.length) {
|
|
10875
|
+
const two = input[i] + input[i + 1];
|
|
10876
|
+
if (two === '==' || two === '!=' || two === '>=' || two === '<=' || two === '&&' || two === '||' || two === '=~' || two === '!~') {
|
|
10877
|
+
tokens.push({ type: 'operator', value: two });
|
|
10878
|
+
i += 2;
|
|
10879
|
+
continue;
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
// Single-character operators
|
|
10883
|
+
if (ch === '>' || ch === '<') {
|
|
10884
|
+
tokens.push({ type: 'operator', value: ch });
|
|
10885
|
+
i++;
|
|
10886
|
+
continue;
|
|
10887
|
+
}
|
|
10888
|
+
// Not operator
|
|
10889
|
+
if (ch === '!') {
|
|
10890
|
+
tokens.push({ type: 'not' });
|
|
10891
|
+
i++;
|
|
10892
|
+
continue;
|
|
10893
|
+
}
|
|
10894
|
+
// String literals
|
|
10895
|
+
if (ch === '"' || ch === "'") {
|
|
10896
|
+
const quote = ch;
|
|
10897
|
+
i++;
|
|
10898
|
+
let str = '';
|
|
10899
|
+
while (i < input.length && input[i] !== quote) {
|
|
10900
|
+
if (input[i] === '\\' && i + 1 < input.length) {
|
|
10901
|
+
i++;
|
|
10902
|
+
str += input[i];
|
|
10903
|
+
}
|
|
10904
|
+
else {
|
|
10905
|
+
str += input[i];
|
|
10906
|
+
}
|
|
10907
|
+
i++;
|
|
10908
|
+
}
|
|
10909
|
+
i++; // skip closing quote
|
|
10910
|
+
tokens.push({ type: 'string', value: str });
|
|
10911
|
+
continue;
|
|
10912
|
+
}
|
|
10913
|
+
// Numbers
|
|
10914
|
+
if (ch >= '0' && ch <= '9') {
|
|
10915
|
+
let num = '';
|
|
10916
|
+
while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
|
|
10917
|
+
num += input[i];
|
|
10918
|
+
i++;
|
|
10919
|
+
}
|
|
10920
|
+
tokens.push({ type: 'number', value: parseFloat(num) });
|
|
10921
|
+
continue;
|
|
10922
|
+
}
|
|
10923
|
+
// Identifiers (including dot notation: customer.isVIP)
|
|
10924
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
10925
|
+
let ident = '';
|
|
10926
|
+
while (i < input.length &&
|
|
10927
|
+
((input[i] >= 'a' && input[i] <= 'z') ||
|
|
10928
|
+
(input[i] >= 'A' && input[i] <= 'Z') ||
|
|
10929
|
+
(input[i] >= '0' && input[i] <= '9') ||
|
|
10930
|
+
input[i] === '_' || input[i] === '$' || input[i] === '.')) {
|
|
10931
|
+
ident += input[i];
|
|
10932
|
+
i++;
|
|
10933
|
+
}
|
|
10934
|
+
if (ident === 'true') {
|
|
10935
|
+
tokens.push({ type: 'boolean', value: true });
|
|
10936
|
+
}
|
|
10937
|
+
else if (ident === 'false') {
|
|
10938
|
+
tokens.push({ type: 'boolean', value: false });
|
|
10939
|
+
}
|
|
10940
|
+
else {
|
|
10941
|
+
tokens.push({ type: 'identifier', value: ident });
|
|
10942
|
+
}
|
|
10943
|
+
continue;
|
|
10944
|
+
}
|
|
10945
|
+
// Unknown character — skip
|
|
10946
|
+
i++;
|
|
10947
|
+
}
|
|
10948
|
+
tokens.push({ type: 'eof' });
|
|
10949
|
+
return tokens;
|
|
10950
|
+
}
|
|
10951
|
+
class Parser {
|
|
10952
|
+
constructor(tokens, data) {
|
|
10953
|
+
this.pos = 0;
|
|
10954
|
+
this.tokens = tokens;
|
|
10955
|
+
this.data = data;
|
|
10956
|
+
}
|
|
10957
|
+
peek() {
|
|
10958
|
+
return this.tokens[this.pos];
|
|
10959
|
+
}
|
|
10960
|
+
advance() {
|
|
10961
|
+
const token = this.tokens[this.pos];
|
|
10962
|
+
this.pos++;
|
|
10963
|
+
return token;
|
|
10964
|
+
}
|
|
10965
|
+
/**
|
|
10966
|
+
* Parse the full expression.
|
|
10967
|
+
* Grammar:
|
|
10968
|
+
* expr → or_expr
|
|
10969
|
+
* or_expr → and_expr ('||' and_expr)*
|
|
10970
|
+
* and_expr → unary (('==' | '!=' | '>' | '<' | '>=' | '<=') unary)?
|
|
10971
|
+
* | unary ('&&' unary_or_comparison)*
|
|
10972
|
+
* unary → '!' unary | primary
|
|
10973
|
+
* primary → '(' expr ')' | literal | identifier
|
|
10974
|
+
*/
|
|
10975
|
+
parse() {
|
|
10976
|
+
const result = this.parseOr();
|
|
10977
|
+
return result;
|
|
10978
|
+
}
|
|
10979
|
+
parseOr() {
|
|
10980
|
+
let left = this.parseAnd();
|
|
10981
|
+
while (this.peek().type === 'operator' && this.peek().value === '||') {
|
|
10982
|
+
this.advance();
|
|
10983
|
+
const right = this.parseAnd();
|
|
10984
|
+
left = this.isTruthy(left) || this.isTruthy(right);
|
|
10985
|
+
}
|
|
10986
|
+
return left;
|
|
10987
|
+
}
|
|
10988
|
+
parseAnd() {
|
|
10989
|
+
let left = this.parseComparison();
|
|
10990
|
+
while (this.peek().type === 'operator' && this.peek().value === '&&') {
|
|
10991
|
+
this.advance();
|
|
10992
|
+
const right = this.parseComparison();
|
|
10993
|
+
left = this.isTruthy(left) && this.isTruthy(right);
|
|
10994
|
+
}
|
|
10995
|
+
return left;
|
|
10996
|
+
}
|
|
10997
|
+
parseComparison() {
|
|
10998
|
+
const left = this.parseUnary();
|
|
10999
|
+
const token = this.peek();
|
|
11000
|
+
if (token.type === 'operator') {
|
|
11001
|
+
const op = token.value;
|
|
11002
|
+
if (op === '==' || op === '!=' || op === '>' || op === '<' || op === '>=' || op === '<=' || op === '=~' || op === '!~') {
|
|
11003
|
+
this.advance();
|
|
11004
|
+
const right = this.parseUnary();
|
|
11005
|
+
return this.compare(left, op, right);
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
return left;
|
|
11009
|
+
}
|
|
11010
|
+
parseUnary() {
|
|
11011
|
+
if (this.peek().type === 'not') {
|
|
11012
|
+
this.advance();
|
|
11013
|
+
const value = this.parseUnary();
|
|
11014
|
+
return !this.isTruthy(value);
|
|
11015
|
+
}
|
|
11016
|
+
return this.parsePrimary();
|
|
11017
|
+
}
|
|
11018
|
+
parsePrimary() {
|
|
11019
|
+
const token = this.peek();
|
|
11020
|
+
if (token.type === 'paren' && token.value === '(') {
|
|
11021
|
+
this.advance();
|
|
11022
|
+
const value = this.parseOr();
|
|
11023
|
+
// Consume closing paren
|
|
11024
|
+
if (this.peek().type === 'paren' && this.peek().value === ')') {
|
|
11025
|
+
this.advance();
|
|
11026
|
+
}
|
|
11027
|
+
return value;
|
|
11028
|
+
}
|
|
11029
|
+
if (token.type === 'string') {
|
|
11030
|
+
this.advance();
|
|
11031
|
+
return token.value;
|
|
11032
|
+
}
|
|
11033
|
+
if (token.type === 'number') {
|
|
11034
|
+
this.advance();
|
|
11035
|
+
return token.value;
|
|
11036
|
+
}
|
|
11037
|
+
if (token.type === 'boolean') {
|
|
11038
|
+
this.advance();
|
|
11039
|
+
return token.value;
|
|
11040
|
+
}
|
|
11041
|
+
if (token.type === 'identifier') {
|
|
11042
|
+
this.advance();
|
|
11043
|
+
return this.resolveIdentifier(token.value);
|
|
11044
|
+
}
|
|
11045
|
+
// EOF or unexpected — return undefined
|
|
11046
|
+
this.advance();
|
|
11047
|
+
return undefined;
|
|
11048
|
+
}
|
|
11049
|
+
resolveIdentifier(path) {
|
|
11050
|
+
const parts = path.split('.');
|
|
11051
|
+
let current = this.data;
|
|
11052
|
+
for (const part of parts) {
|
|
11053
|
+
if (current === null || current === undefined) {
|
|
11054
|
+
return undefined;
|
|
11055
|
+
}
|
|
11056
|
+
if (typeof current === 'object') {
|
|
11057
|
+
current = current[part];
|
|
11058
|
+
}
|
|
11059
|
+
else {
|
|
11060
|
+
return undefined;
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
11063
|
+
return current;
|
|
11064
|
+
}
|
|
11065
|
+
compare(left, op, right) {
|
|
11066
|
+
// Regex match: left is coerced to string, right is the pattern string
|
|
11067
|
+
if (op === '=~' || op === '!~') {
|
|
11068
|
+
const str = this.toString(left);
|
|
11069
|
+
const pattern = this.toString(right);
|
|
11070
|
+
try {
|
|
11071
|
+
const regex = new RegExp(pattern);
|
|
11072
|
+
const matches = regex.test(str);
|
|
11073
|
+
return op === '=~' ? matches : !matches;
|
|
11074
|
+
}
|
|
11075
|
+
catch {
|
|
11076
|
+
// Invalid regex pattern — treat as no match
|
|
11077
|
+
return op === '!~';
|
|
11078
|
+
}
|
|
11079
|
+
}
|
|
11080
|
+
// For ordering operators, coerce both sides to numbers if either side is numeric
|
|
11081
|
+
if (op === '>' || op === '<' || op === '>=' || op === '<=') {
|
|
11082
|
+
const l = this.toNumber(left);
|
|
11083
|
+
const r = this.toNumber(right);
|
|
11084
|
+
switch (op) {
|
|
11085
|
+
case '>': return l > r;
|
|
11086
|
+
case '<': return l < r;
|
|
11087
|
+
case '>=': return l >= r;
|
|
11088
|
+
case '<=': return l <= r;
|
|
11089
|
+
}
|
|
11090
|
+
}
|
|
11091
|
+
// For equality, coerce to numbers if both sides look numeric
|
|
11092
|
+
const ln = this.toNumberIfNumeric(left);
|
|
11093
|
+
const rn = this.toNumberIfNumeric(right);
|
|
11094
|
+
switch (op) {
|
|
11095
|
+
case '==': return ln == rn; // eslint-disable-line eqeqeq
|
|
11096
|
+
case '!=': return ln != rn; // eslint-disable-line eqeqeq
|
|
11097
|
+
default: return false;
|
|
11098
|
+
}
|
|
11099
|
+
}
|
|
11100
|
+
/**
|
|
11101
|
+
* Convert a value to a string for regex matching.
|
|
11102
|
+
*/
|
|
11103
|
+
toString(value) {
|
|
11104
|
+
if (value === null || value === undefined)
|
|
11105
|
+
return '';
|
|
11106
|
+
if (typeof value === 'string')
|
|
11107
|
+
return value;
|
|
11108
|
+
return String(value);
|
|
11109
|
+
}
|
|
11110
|
+
/**
|
|
11111
|
+
* Convert a value to a number. Strings that look like numbers are parsed.
|
|
11112
|
+
* Non-numeric values become NaN.
|
|
11113
|
+
*/
|
|
11114
|
+
toNumber(value) {
|
|
11115
|
+
if (typeof value === 'number')
|
|
11116
|
+
return value;
|
|
11117
|
+
if (typeof value === 'string') {
|
|
11118
|
+
const n = Number(value);
|
|
11119
|
+
return isNaN(n) ? NaN : n;
|
|
11120
|
+
}
|
|
11121
|
+
if (typeof value === 'boolean')
|
|
11122
|
+
return value ? 1 : 0;
|
|
11123
|
+
return NaN;
|
|
11124
|
+
}
|
|
11125
|
+
/**
|
|
11126
|
+
* If a value is a string that looks like a number, convert it.
|
|
11127
|
+
* Otherwise return the value as-is. Used for == / != so that
|
|
11128
|
+
* "5" == 5 is true but "hello" == "hello" still works.
|
|
11129
|
+
*/
|
|
11130
|
+
toNumberIfNumeric(value) {
|
|
11131
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
11132
|
+
const n = Number(value);
|
|
11133
|
+
if (!isNaN(n))
|
|
11134
|
+
return n;
|
|
11135
|
+
}
|
|
11136
|
+
return value;
|
|
11137
|
+
}
|
|
11138
|
+
isTruthy(value) {
|
|
11139
|
+
if (value === null || value === undefined)
|
|
11140
|
+
return false;
|
|
11141
|
+
if (typeof value === 'boolean')
|
|
11142
|
+
return value;
|
|
11143
|
+
if (typeof value === 'number')
|
|
11144
|
+
return value !== 0;
|
|
11145
|
+
if (typeof value === 'string')
|
|
11146
|
+
return value.length > 0;
|
|
11147
|
+
if (Array.isArray(value))
|
|
11148
|
+
return value.length > 0;
|
|
11149
|
+
return true;
|
|
11150
|
+
}
|
|
11151
|
+
}
|
|
11152
|
+
/**
|
|
11153
|
+
* Static predicate evaluator for conditional sections.
|
|
11154
|
+
*/
|
|
11155
|
+
class PredicateEvaluator {
|
|
11156
|
+
/**
|
|
11157
|
+
* Evaluate a predicate expression against data.
|
|
11158
|
+
* @param predicate The predicate string (e.g., "isActive", "count > 0")
|
|
11159
|
+
* @param data The data context to evaluate against
|
|
11160
|
+
* @returns true if the predicate is truthy, false otherwise
|
|
11161
|
+
*/
|
|
11162
|
+
static evaluate(predicate, data) {
|
|
11163
|
+
if (!predicate || predicate.trim().length === 0) {
|
|
11164
|
+
return false;
|
|
11165
|
+
}
|
|
11166
|
+
try {
|
|
11167
|
+
const tokens = tokenize(predicate.trim());
|
|
11168
|
+
const parser = new Parser(tokens, data);
|
|
11169
|
+
const result = parser.parse();
|
|
11170
|
+
// Convert result to boolean
|
|
11171
|
+
if (result === null || result === undefined)
|
|
11172
|
+
return false;
|
|
11173
|
+
if (typeof result === 'boolean')
|
|
11174
|
+
return result;
|
|
11175
|
+
if (typeof result === 'number')
|
|
11176
|
+
return result !== 0;
|
|
11177
|
+
if (typeof result === 'string')
|
|
11178
|
+
return result.length > 0;
|
|
11179
|
+
if (Array.isArray(result))
|
|
11180
|
+
return result.length > 0;
|
|
11181
|
+
return true;
|
|
11182
|
+
}
|
|
11183
|
+
catch {
|
|
11184
|
+
// If evaluation fails, treat as false
|
|
11185
|
+
return false;
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
|
|
10120
11190
|
/**
|
|
10121
11191
|
* Abstract base class providing common functionality for regions.
|
|
10122
11192
|
*/
|
|
@@ -11152,6 +12222,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
|
|
|
11152
12222
|
const LOOP_LABEL_PADDING = 4;
|
|
11153
12223
|
const LOOP_LABEL_RADIUS = 4;
|
|
11154
12224
|
const LOOP_LINE_DASH = [4, 4];
|
|
12225
|
+
// Conditional section indicator styling
|
|
12226
|
+
const COND_INDICATOR_COLOR = '#D97706'; // Orange
|
|
12227
|
+
const COND_LABEL_PADDING = 4;
|
|
12228
|
+
const COND_LABEL_RADIUS = 4;
|
|
12229
|
+
const COND_LINE_DASH = [4, 4];
|
|
11155
12230
|
// Hyperlink styling
|
|
11156
12231
|
const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
|
|
11157
12232
|
class FlowingTextRenderer extends EventEmitter {
|
|
@@ -11409,8 +12484,6 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11409
12484
|
if (pageIndex === 0) {
|
|
11410
12485
|
// Clear table continuations when starting a new render cycle
|
|
11411
12486
|
this.clearTableContinuations();
|
|
11412
|
-
// Clear content hit targets - they will be re-registered during render
|
|
11413
|
-
this._hitTestManager.clearCategory('content');
|
|
11414
12487
|
// This is the first page, flow all text
|
|
11415
12488
|
const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
|
|
11416
12489
|
this.flowedPages.set(page.id, flowedPages);
|
|
@@ -11710,6 +12783,8 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11710
12783
|
const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
|
|
11711
12784
|
// Get hyperlinks for rendering
|
|
11712
12785
|
const hyperlinks = flowingContent.getAllHyperlinks();
|
|
12786
|
+
// Track relative objects to render after all lines (so they appear on top)
|
|
12787
|
+
const relativeObjects = [];
|
|
11713
12788
|
// Render each line
|
|
11714
12789
|
let y = bounds.y;
|
|
11715
12790
|
for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
|
|
@@ -11722,6 +12797,18 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11722
12797
|
if (clipToBounds && y > bounds.y + bounds.height) {
|
|
11723
12798
|
break;
|
|
11724
12799
|
}
|
|
12800
|
+
// Collect relative objects from this line
|
|
12801
|
+
if (line.embeddedObjects) {
|
|
12802
|
+
for (const embeddedObj of line.embeddedObjects) {
|
|
12803
|
+
if (embeddedObj.isAnchor && embeddedObj.object.position === 'relative') {
|
|
12804
|
+
relativeObjects.push({
|
|
12805
|
+
object: embeddedObj.object,
|
|
12806
|
+
anchorX: bounds.x,
|
|
12807
|
+
anchorY: y
|
|
12808
|
+
});
|
|
12809
|
+
}
|
|
12810
|
+
}
|
|
12811
|
+
}
|
|
11725
12812
|
this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
|
|
11726
12813
|
y += line.height;
|
|
11727
12814
|
}
|
|
@@ -11732,6 +12819,10 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11732
12819
|
if (clipToBounds) {
|
|
11733
12820
|
ctx.restore();
|
|
11734
12821
|
}
|
|
12822
|
+
// Render relative objects on top of text (outside clip region)
|
|
12823
|
+
if (relativeObjects.length > 0) {
|
|
12824
|
+
this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
|
|
12825
|
+
}
|
|
11735
12826
|
}
|
|
11736
12827
|
/**
|
|
11737
12828
|
* Render selection highlight for a region.
|
|
@@ -13887,43 +14978,256 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13887
14978
|
}
|
|
13888
14979
|
y += line.height;
|
|
13889
14980
|
}
|
|
13890
|
-
// Check if text index is just past the last line (end of content)
|
|
13891
|
-
if (flowedPage.lines.length > 0) {
|
|
13892
|
-
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
13893
|
-
if (textIndex === lastLine.endIndex + 1) {
|
|
13894
|
-
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
13895
|
-
}
|
|
14981
|
+
// Check if text index is just past the last line (end of content)
|
|
14982
|
+
if (flowedPage.lines.length > 0) {
|
|
14983
|
+
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
14984
|
+
if (textIndex === lastLine.endIndex + 1) {
|
|
14985
|
+
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
14986
|
+
}
|
|
14987
|
+
}
|
|
14988
|
+
return null;
|
|
14989
|
+
}
|
|
14990
|
+
/**
|
|
14991
|
+
* Check if a section spans across a flowed page (starts before and ends after).
|
|
14992
|
+
*/
|
|
14993
|
+
sectionSpansPage(section, flowedPage) {
|
|
14994
|
+
if (flowedPage.lines.length === 0)
|
|
14995
|
+
return false;
|
|
14996
|
+
const pageStart = flowedPage.startIndex;
|
|
14997
|
+
const pageEnd = flowedPage.endIndex;
|
|
14998
|
+
// Section spans this page if it started before and ends after
|
|
14999
|
+
return section.startIndex < pageStart && section.endIndex > pageEnd;
|
|
15000
|
+
}
|
|
15001
|
+
/**
|
|
15002
|
+
* Get a repeating section at a point (for click detection).
|
|
15003
|
+
* Checks if the point is on the Loop label or vertical connector.
|
|
15004
|
+
*/
|
|
15005
|
+
getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
|
|
15006
|
+
const labelX = pageBounds.x + 5;
|
|
15007
|
+
const labelWidth = 32;
|
|
15008
|
+
const connectorX = labelX + labelWidth / 2;
|
|
15009
|
+
const hitRadius = 10; // Pixels for click detection
|
|
15010
|
+
for (const section of sections) {
|
|
15011
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
15012
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
15013
|
+
const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
|
|
15014
|
+
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
15015
|
+
continue;
|
|
15016
|
+
}
|
|
15017
|
+
// Check if click is on the Loop label
|
|
15018
|
+
if (startInfo) {
|
|
15019
|
+
const labelY = startInfo.y - 10;
|
|
15020
|
+
const labelHeight = 18;
|
|
15021
|
+
if (point.x >= labelX &&
|
|
15022
|
+
point.x <= labelX + labelWidth &&
|
|
15023
|
+
point.y >= labelY &&
|
|
15024
|
+
point.y <= labelY + labelHeight) {
|
|
15025
|
+
return section;
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
// Check if click is on the vertical connector line
|
|
15029
|
+
let verticalStartY;
|
|
15030
|
+
let verticalEndY;
|
|
15031
|
+
if (startInfo) {
|
|
15032
|
+
verticalStartY = startInfo.y;
|
|
15033
|
+
}
|
|
15034
|
+
else {
|
|
15035
|
+
verticalStartY = contentBounds.y;
|
|
15036
|
+
}
|
|
15037
|
+
if (endInfo) {
|
|
15038
|
+
verticalEndY = endInfo.y;
|
|
15039
|
+
}
|
|
15040
|
+
else if (sectionSpansThisPage) {
|
|
15041
|
+
verticalEndY = contentBounds.y + flowedPage.height;
|
|
15042
|
+
}
|
|
15043
|
+
else {
|
|
15044
|
+
continue;
|
|
15045
|
+
}
|
|
15046
|
+
if (Math.abs(point.x - connectorX) <= hitRadius &&
|
|
15047
|
+
point.y >= verticalStartY &&
|
|
15048
|
+
point.y <= verticalEndY) {
|
|
15049
|
+
return section;
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
return null;
|
|
15053
|
+
}
|
|
15054
|
+
// ============================================
|
|
15055
|
+
// Conditional Section Indicators
|
|
15056
|
+
// ============================================
|
|
15057
|
+
/**
|
|
15058
|
+
* Render conditional section indicators for a page.
|
|
15059
|
+
*/
|
|
15060
|
+
renderConditionalSectionIndicators(sections, pageIndex, ctx, contentBounds, flowedPage, pageBounds, selectedSectionId = null) {
|
|
15061
|
+
for (const section of sections) {
|
|
15062
|
+
this.renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, pageBounds, section.id === selectedSectionId);
|
|
15063
|
+
}
|
|
15064
|
+
}
|
|
15065
|
+
/**
|
|
15066
|
+
* Render a single conditional section indicator.
|
|
15067
|
+
*/
|
|
15068
|
+
renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, _pageBounds, isSelected = false) {
|
|
15069
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
15070
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
15071
|
+
const sectionOverlapsPage = section.startIndex < flowedPage.endIndex &&
|
|
15072
|
+
section.endIndex > flowedPage.startIndex;
|
|
15073
|
+
if (!sectionOverlapsPage) {
|
|
15074
|
+
return;
|
|
15075
|
+
}
|
|
15076
|
+
const hasStart = startInfo !== null;
|
|
15077
|
+
const hasEnd = endInfo !== null;
|
|
15078
|
+
const startsBeforePage = section.startIndex < flowedPage.startIndex;
|
|
15079
|
+
const endsAfterPage = section.endIndex > flowedPage.endIndex;
|
|
15080
|
+
ctx.save();
|
|
15081
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
15082
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15083
|
+
ctx.lineWidth = 1;
|
|
15084
|
+
// Position on the right side of the content area
|
|
15085
|
+
const labelWidth = 22;
|
|
15086
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
15087
|
+
const connectorX = labelX + labelWidth / 2;
|
|
15088
|
+
// Draw start indicator lines
|
|
15089
|
+
if (hasStart) {
|
|
15090
|
+
const startY = startInfo.y;
|
|
15091
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15092
|
+
ctx.beginPath();
|
|
15093
|
+
ctx.moveTo(contentBounds.x, startY);
|
|
15094
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, startY);
|
|
15095
|
+
ctx.stroke();
|
|
15096
|
+
ctx.setLineDash([]);
|
|
15097
|
+
ctx.beginPath();
|
|
15098
|
+
ctx.moveTo(contentBounds.x + contentBounds.width, startY);
|
|
15099
|
+
ctx.lineTo(labelX, startY);
|
|
15100
|
+
ctx.stroke();
|
|
15101
|
+
}
|
|
15102
|
+
else if (startsBeforePage) {
|
|
15103
|
+
const topY = contentBounds.y;
|
|
15104
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15105
|
+
ctx.beginPath();
|
|
15106
|
+
ctx.moveTo(contentBounds.x, topY);
|
|
15107
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15108
|
+
ctx.stroke();
|
|
15109
|
+
ctx.setLineDash([]);
|
|
15110
|
+
ctx.beginPath();
|
|
15111
|
+
ctx.moveTo(connectorX, topY);
|
|
15112
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15113
|
+
ctx.stroke();
|
|
15114
|
+
}
|
|
15115
|
+
// Draw end indicator
|
|
15116
|
+
if (hasEnd) {
|
|
15117
|
+
const endY = endInfo.y;
|
|
15118
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15119
|
+
ctx.beginPath();
|
|
15120
|
+
ctx.moveTo(contentBounds.x, endY);
|
|
15121
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15122
|
+
ctx.stroke();
|
|
15123
|
+
ctx.setLineDash([]);
|
|
15124
|
+
ctx.beginPath();
|
|
15125
|
+
ctx.moveTo(connectorX, endY);
|
|
15126
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15127
|
+
ctx.stroke();
|
|
15128
|
+
}
|
|
15129
|
+
else if (endsAfterPage) {
|
|
15130
|
+
const bottomY = contentBounds.y + contentBounds.height;
|
|
15131
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15132
|
+
ctx.beginPath();
|
|
15133
|
+
ctx.moveTo(contentBounds.x, bottomY);
|
|
15134
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15135
|
+
ctx.stroke();
|
|
15136
|
+
ctx.setLineDash([]);
|
|
15137
|
+
ctx.beginPath();
|
|
15138
|
+
ctx.moveTo(connectorX, bottomY);
|
|
15139
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15140
|
+
ctx.stroke();
|
|
15141
|
+
}
|
|
15142
|
+
// Draw vertical connector line
|
|
15143
|
+
let verticalStartY;
|
|
15144
|
+
let verticalEndY;
|
|
15145
|
+
if (hasStart) {
|
|
15146
|
+
verticalStartY = startInfo.y;
|
|
15147
|
+
}
|
|
15148
|
+
else if (startsBeforePage) {
|
|
15149
|
+
verticalStartY = contentBounds.y;
|
|
15150
|
+
}
|
|
15151
|
+
else {
|
|
15152
|
+
verticalStartY = contentBounds.y;
|
|
15153
|
+
}
|
|
15154
|
+
if (hasEnd) {
|
|
15155
|
+
verticalEndY = endInfo.y;
|
|
15156
|
+
}
|
|
15157
|
+
else if (endsAfterPage) {
|
|
15158
|
+
verticalEndY = contentBounds.y + contentBounds.height;
|
|
15159
|
+
}
|
|
15160
|
+
else {
|
|
15161
|
+
verticalEndY = verticalStartY;
|
|
13896
15162
|
}
|
|
13897
|
-
|
|
15163
|
+
if (verticalEndY > verticalStartY) {
|
|
15164
|
+
ctx.beginPath();
|
|
15165
|
+
ctx.moveTo(connectorX, verticalStartY);
|
|
15166
|
+
ctx.lineTo(connectorX, verticalEndY);
|
|
15167
|
+
ctx.stroke();
|
|
15168
|
+
}
|
|
15169
|
+
// Draw "If" label
|
|
15170
|
+
if (hasStart) {
|
|
15171
|
+
const startY = startInfo.y;
|
|
15172
|
+
this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
|
|
15173
|
+
}
|
|
15174
|
+
// Update visual state
|
|
15175
|
+
section.visualState = {
|
|
15176
|
+
startPageIndex: hasStart ? pageIndex : -1,
|
|
15177
|
+
startY: hasStart ? startInfo.y : 0,
|
|
15178
|
+
endPageIndex: hasEnd ? pageIndex : -1,
|
|
15179
|
+
endY: hasEnd ? endInfo.y : 0,
|
|
15180
|
+
spansMultiplePages: !hasStart || !hasEnd
|
|
15181
|
+
};
|
|
15182
|
+
ctx.restore();
|
|
13898
15183
|
}
|
|
13899
15184
|
/**
|
|
13900
|
-
*
|
|
15185
|
+
* Draw the "If" label in a rounded rectangle.
|
|
13901
15186
|
*/
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
const
|
|
13906
|
-
const
|
|
13907
|
-
|
|
13908
|
-
|
|
15187
|
+
drawCondLabel(ctx, x, y, text, isSelected = false) {
|
|
15188
|
+
ctx.save();
|
|
15189
|
+
ctx.font = '10px Arial';
|
|
15190
|
+
const metrics = ctx.measureText(text);
|
|
15191
|
+
const textWidth = metrics.width;
|
|
15192
|
+
const textHeight = 10;
|
|
15193
|
+
const boxWidth = textWidth + COND_LABEL_PADDING * 2;
|
|
15194
|
+
const boxHeight = textHeight + COND_LABEL_PADDING * 2;
|
|
15195
|
+
ctx.beginPath();
|
|
15196
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
|
|
15197
|
+
if (isSelected) {
|
|
15198
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15199
|
+
ctx.fill();
|
|
15200
|
+
ctx.fillStyle = '#ffffff';
|
|
15201
|
+
}
|
|
15202
|
+
else {
|
|
15203
|
+
ctx.fillStyle = '#ffffff';
|
|
15204
|
+
ctx.fill();
|
|
15205
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
15206
|
+
ctx.lineWidth = 1.5;
|
|
15207
|
+
ctx.stroke();
|
|
15208
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15209
|
+
}
|
|
15210
|
+
ctx.textBaseline = 'middle';
|
|
15211
|
+
ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
|
|
15212
|
+
ctx.restore();
|
|
13909
15213
|
}
|
|
13910
15214
|
/**
|
|
13911
|
-
* Get a
|
|
13912
|
-
* Checks if the point is on the Loop label or vertical connector.
|
|
15215
|
+
* Get a conditional section at a point (for click detection).
|
|
13913
15216
|
*/
|
|
13914
|
-
|
|
13915
|
-
const
|
|
13916
|
-
const
|
|
15217
|
+
getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
|
|
15218
|
+
const labelWidth = 22;
|
|
15219
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
13917
15220
|
const connectorX = labelX + labelWidth / 2;
|
|
13918
|
-
const hitRadius = 10;
|
|
15221
|
+
const hitRadius = 10;
|
|
13919
15222
|
for (const section of sections) {
|
|
13920
15223
|
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
13921
15224
|
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
13922
|
-
const sectionSpansThisPage =
|
|
15225
|
+
const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
|
|
15226
|
+
section.endIndex > flowedPage.endIndex;
|
|
13923
15227
|
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
13924
15228
|
continue;
|
|
13925
15229
|
}
|
|
13926
|
-
// Check if click is on the
|
|
15230
|
+
// Check if click is on the "If" label
|
|
13927
15231
|
if (startInfo) {
|
|
13928
15232
|
const labelY = startInfo.y - 10;
|
|
13929
15233
|
const labelHeight = 18;
|
|
@@ -13988,6 +15292,7 @@ class CanvasManager extends EventEmitter {
|
|
|
13988
15292
|
this.isSelectingText = false;
|
|
13989
15293
|
this.textSelectionStartPageId = null;
|
|
13990
15294
|
this.selectedSectionId = null;
|
|
15295
|
+
this.selectedConditionalSectionId = null;
|
|
13991
15296
|
this._activeSection = 'body';
|
|
13992
15297
|
this.lastClickTime = 0;
|
|
13993
15298
|
this.lastClickPosition = null;
|
|
@@ -14127,6 +15432,11 @@ class CanvasManager extends EventEmitter {
|
|
|
14127
15432
|
}
|
|
14128
15433
|
// 2. CONTENT: Render all text and elements
|
|
14129
15434
|
const pageIndex = this.document.pages.findIndex(p => p.id === page.id);
|
|
15435
|
+
// Clear content hit targets before rendering all sections (header, body, footer)
|
|
15436
|
+
// so that each section's hit targets are re-registered during render
|
|
15437
|
+
if (pageIndex === 0) {
|
|
15438
|
+
this.flowingTextRenderer.hitTestManager.clearCategory('content');
|
|
15439
|
+
}
|
|
14130
15440
|
// Render header content
|
|
14131
15441
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
14132
15442
|
this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
|
|
@@ -14154,6 +15464,16 @@ class CanvasManager extends EventEmitter {
|
|
|
14154
15464
|
this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
|
|
14155
15465
|
}
|
|
14156
15466
|
}
|
|
15467
|
+
// Render conditional section indicators (only in body)
|
|
15468
|
+
const condSections = bodyFlowingContent?.getConditionalSections() ?? [];
|
|
15469
|
+
if (condSections.length > 0) {
|
|
15470
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
15471
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
15472
|
+
const pageDimensions = page.getPageDimensions();
|
|
15473
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
15474
|
+
this.flowingTextRenderer.renderConditionalSectionIndicators(condSections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedConditionalSectionId);
|
|
15475
|
+
}
|
|
15476
|
+
}
|
|
14157
15477
|
// Render all elements (without selection marks)
|
|
14158
15478
|
this.renderPageElements(page, ctx);
|
|
14159
15479
|
// 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
|
|
@@ -14314,7 +15634,8 @@ class CanvasManager extends EventEmitter {
|
|
|
14314
15634
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
14315
15635
|
// Get the slice for this page (for multi-page tables)
|
|
14316
15636
|
const slice = table.getRenderedSlice(pageIndex);
|
|
14317
|
-
const tablePosition = slice?.position ||
|
|
15637
|
+
const tablePosition = slice?.position ||
|
|
15638
|
+
(table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
|
|
14318
15639
|
const sliceHeight = slice?.height || table.height;
|
|
14319
15640
|
// Check if point is within the table slice on this page
|
|
14320
15641
|
const isInsideTable = tablePosition &&
|
|
@@ -14347,6 +15668,7 @@ class CanvasManager extends EventEmitter {
|
|
|
14347
15668
|
end: cellAddr
|
|
14348
15669
|
});
|
|
14349
15670
|
this.render();
|
|
15671
|
+
this.emit('table-cell-selection-changed', { table });
|
|
14350
15672
|
e.preventDefault();
|
|
14351
15673
|
return;
|
|
14352
15674
|
}
|
|
@@ -14449,11 +15771,10 @@ class CanvasManager extends EventEmitter {
|
|
|
14449
15771
|
const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
|
|
14450
15772
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14451
15773
|
const object = embeddedObjectHit.data.object;
|
|
14452
|
-
//
|
|
15774
|
+
// If object is in a different section, switch to that section first
|
|
14453
15775
|
const objectSection = this.getSectionForEmbeddedObject(object);
|
|
14454
15776
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14455
|
-
|
|
14456
|
-
return;
|
|
15777
|
+
this.setActiveSection(objectSection);
|
|
14457
15778
|
}
|
|
14458
15779
|
// For relative-positioned objects, prepare for potential drag
|
|
14459
15780
|
// Don't start drag immediately - wait for threshold to allow double-click
|
|
@@ -14626,7 +15947,8 @@ class CanvasManager extends EventEmitter {
|
|
|
14626
15947
|
const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
14627
15948
|
// Get the slice for the current page (for multi-page tables)
|
|
14628
15949
|
const slice = table.getRenderedSlice(currentPageIndex);
|
|
14629
|
-
const tablePosition = slice?.position ||
|
|
15950
|
+
const tablePosition = slice?.position ||
|
|
15951
|
+
(table.renderedPageIndex === currentPageIndex ? table.renderedPosition : null);
|
|
14630
15952
|
const sliceHeight = slice?.height || table.height;
|
|
14631
15953
|
if (tablePosition) {
|
|
14632
15954
|
// Check if point is within the table slice on this page
|
|
@@ -14656,6 +15978,7 @@ class CanvasManager extends EventEmitter {
|
|
|
14656
15978
|
end: cellAddr
|
|
14657
15979
|
});
|
|
14658
15980
|
this.render();
|
|
15981
|
+
this.emit('table-cell-selection-changed', { table });
|
|
14659
15982
|
}
|
|
14660
15983
|
}
|
|
14661
15984
|
}
|
|
@@ -14938,14 +16261,12 @@ class CanvasManager extends EventEmitter {
|
|
|
14938
16261
|
const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
|
|
14939
16262
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14940
16263
|
const clickedObject = embeddedObjectHit.data.object;
|
|
14941
|
-
//
|
|
16264
|
+
// If object is in a different section, switch to that section first
|
|
14942
16265
|
const objectSection = this.getSectionForEmbeddedObject(clickedObject);
|
|
14943
|
-
// Only allow selection if object is in the active section
|
|
14944
16266
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14945
|
-
|
|
14946
|
-
return;
|
|
16267
|
+
this.setActiveSection(objectSection);
|
|
14947
16268
|
}
|
|
14948
|
-
// Clicked on embedded object
|
|
16269
|
+
// Clicked on embedded object - clear text selection and select it
|
|
14949
16270
|
const activeFlowingContent = this.getFlowingContentForActiveSection();
|
|
14950
16271
|
if (activeFlowingContent) {
|
|
14951
16272
|
activeFlowingContent.clearSelection();
|
|
@@ -14982,6 +16303,64 @@ class CanvasManager extends EventEmitter {
|
|
|
14982
16303
|
}
|
|
14983
16304
|
}
|
|
14984
16305
|
}
|
|
16306
|
+
// Check if we clicked on a conditional section indicator
|
|
16307
|
+
if (bodyFlowingContent) {
|
|
16308
|
+
const condSections = bodyFlowingContent.getConditionalSections();
|
|
16309
|
+
if (condSections.length > 0 && page) {
|
|
16310
|
+
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
16311
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
16312
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
16313
|
+
const contentBounds = page.getContentBounds();
|
|
16314
|
+
const contentRect = {
|
|
16315
|
+
x: contentBounds.position.x,
|
|
16316
|
+
y: contentBounds.position.y,
|
|
16317
|
+
width: contentBounds.size.width,
|
|
16318
|
+
height: contentBounds.size.height
|
|
16319
|
+
};
|
|
16320
|
+
const pageDimensions = page.getPageDimensions();
|
|
16321
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
16322
|
+
const clickedCondSection = this.flowingTextRenderer.getConditionalSectionAtPoint(point, condSections, pageIndex, pageBounds, contentRect, flowedPages[pageIndex]);
|
|
16323
|
+
if (clickedCondSection) {
|
|
16324
|
+
this.clearSelection();
|
|
16325
|
+
this.selectedConditionalSectionId = clickedCondSection.id;
|
|
16326
|
+
this.render();
|
|
16327
|
+
this.emit('conditional-section-clicked', { section: clickedCondSection });
|
|
16328
|
+
return;
|
|
16329
|
+
}
|
|
16330
|
+
}
|
|
16331
|
+
}
|
|
16332
|
+
}
|
|
16333
|
+
// Check if we clicked on a table row loop label
|
|
16334
|
+
const clickedPageIdx = this.document.pages.findIndex(p => p.id === pageId);
|
|
16335
|
+
const bodyContent = this.document.bodyFlowingContent;
|
|
16336
|
+
if (bodyContent) {
|
|
16337
|
+
const embeddedObjects = bodyContent.getEmbeddedObjects();
|
|
16338
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
16339
|
+
if (obj instanceof TableObject && obj.renderedPosition && obj.renderedPageIndex === clickedPageIdx) {
|
|
16340
|
+
// Convert to table-local coordinates
|
|
16341
|
+
const localPoint = {
|
|
16342
|
+
x: point.x - obj.renderedPosition.x,
|
|
16343
|
+
y: point.y - obj.renderedPosition.y
|
|
16344
|
+
};
|
|
16345
|
+
const clickedLoop = obj.getRowLoopAtPoint(localPoint);
|
|
16346
|
+
if (clickedLoop) {
|
|
16347
|
+
// Select this loop
|
|
16348
|
+
obj.selectRowLoop(clickedLoop.id);
|
|
16349
|
+
this.render();
|
|
16350
|
+
this.emit('table-row-loop-clicked', { table: obj, loop: clickedLoop });
|
|
16351
|
+
return;
|
|
16352
|
+
}
|
|
16353
|
+
// Check for row conditional click
|
|
16354
|
+
const clickedCond = obj.getRowConditionalAtPoint(localPoint);
|
|
16355
|
+
if (clickedCond) {
|
|
16356
|
+
obj.selectRowConditional(clickedCond.id);
|
|
16357
|
+
this.render();
|
|
16358
|
+
this.emit('table-row-conditional-clicked', { table: obj, conditional: clickedCond });
|
|
16359
|
+
return;
|
|
16360
|
+
}
|
|
16361
|
+
}
|
|
16362
|
+
}
|
|
16363
|
+
}
|
|
14985
16364
|
// If no regular element was clicked, try flowing text using unified region click handler
|
|
14986
16365
|
const ctx = this.contexts.get(pageId);
|
|
14987
16366
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
@@ -15191,48 +16570,49 @@ class CanvasManager extends EventEmitter {
|
|
|
15191
16570
|
const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
|
|
15192
16571
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
15193
16572
|
const object = embeddedObjectHit.data.object;
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
16573
|
+
if (object.position === 'relative') {
|
|
16574
|
+
canvas.style.cursor = 'move';
|
|
16575
|
+
return;
|
|
16576
|
+
}
|
|
16577
|
+
// Show text cursor for objects in edit mode, arrow otherwise
|
|
16578
|
+
if (object instanceof TextBoxObject && this.editingTextBox === object) {
|
|
16579
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16580
|
+
}
|
|
16581
|
+
else if (object instanceof TableObject && this._focusedControl === object) {
|
|
16582
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16583
|
+
}
|
|
15197
16584
|
else {
|
|
15198
|
-
|
|
15199
|
-
canvas.style.cursor = 'move';
|
|
15200
|
-
return;
|
|
15201
|
-
}
|
|
15202
|
-
// Show text cursor for text boxes
|
|
15203
|
-
if (object instanceof TextBoxObject) {
|
|
15204
|
-
canvas.style.cursor = 'text';
|
|
15205
|
-
return;
|
|
15206
|
-
}
|
|
16585
|
+
canvas.style.cursor = 'default';
|
|
15207
16586
|
}
|
|
16587
|
+
return;
|
|
15208
16588
|
}
|
|
15209
16589
|
// Check for table cells (show text cursor)
|
|
15210
16590
|
const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
|
|
15211
16591
|
if (tableCellHit && tableCellHit.data.type === 'table-cell') {
|
|
15212
|
-
canvas.style.cursor =
|
|
16592
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15213
16593
|
return;
|
|
15214
16594
|
}
|
|
15215
16595
|
// Check for text regions (body, header, footer - show text cursor)
|
|
15216
16596
|
const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
|
|
15217
16597
|
if (textRegionHit && textRegionHit.data.type === 'text-region') {
|
|
15218
|
-
canvas.style.cursor =
|
|
16598
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15219
16599
|
return;
|
|
15220
16600
|
}
|
|
15221
16601
|
// Also check if point is within any editable region (body, header, footer)
|
|
15222
16602
|
// This catches cases where text region hit targets may not cover empty space
|
|
15223
16603
|
const bodyRegion = this.regionManager.getBodyRegion();
|
|
15224
16604
|
if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
|
|
15225
|
-
canvas.style.cursor =
|
|
16605
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15226
16606
|
return;
|
|
15227
16607
|
}
|
|
15228
16608
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
15229
16609
|
if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
|
|
15230
|
-
canvas.style.cursor =
|
|
16610
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15231
16611
|
return;
|
|
15232
16612
|
}
|
|
15233
16613
|
const footerRegion = this.regionManager.getFooterRegion();
|
|
15234
16614
|
if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
|
|
15235
|
-
canvas.style.cursor =
|
|
16615
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15236
16616
|
return;
|
|
15237
16617
|
}
|
|
15238
16618
|
canvas.style.cursor = 'default';
|
|
@@ -15250,7 +16630,8 @@ class CanvasManager extends EventEmitter {
|
|
|
15250
16630
|
const { table, dividerType, index } = target.data;
|
|
15251
16631
|
// Get the table position from slice info
|
|
15252
16632
|
const slice = table.getRenderedSlice(pageIndex);
|
|
15253
|
-
const tablePosition = slice?.position ||
|
|
16633
|
+
const tablePosition = slice?.position ||
|
|
16634
|
+
(table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
|
|
15254
16635
|
if (tablePosition) {
|
|
15255
16636
|
// Calculate the divider position based on type and index
|
|
15256
16637
|
let position;
|
|
@@ -15393,6 +16774,7 @@ class CanvasManager extends EventEmitter {
|
|
|
15393
16774
|
});
|
|
15394
16775
|
this.selectedElements.clear();
|
|
15395
16776
|
this.selectedSectionId = null;
|
|
16777
|
+
this.selectedConditionalSectionId = null;
|
|
15396
16778
|
Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
|
|
15397
16779
|
this.render();
|
|
15398
16780
|
this.updateResizeHandleHitTargets();
|
|
@@ -15978,7 +17360,9 @@ class CanvasManager extends EventEmitter {
|
|
|
15978
17360
|
if (obj instanceof TableObject) {
|
|
15979
17361
|
// For multi-page tables, check if this page has a rendered slice
|
|
15980
17362
|
const slice = obj.getRenderedSlice(pageIndex);
|
|
15981
|
-
|
|
17363
|
+
// Only use renderedPosition if the table was actually rendered on this page
|
|
17364
|
+
const tablePosition = slice?.position ||
|
|
17365
|
+
(obj.renderedPageIndex === pageIndex ? obj.renderedPosition : null);
|
|
15982
17366
|
if (tablePosition) {
|
|
15983
17367
|
// Check if point is inside the table slice on this page
|
|
15984
17368
|
const sliceHeight = slice?.height || obj.height;
|
|
@@ -16222,6 +17606,10 @@ class CanvasManager extends EventEmitter {
|
|
|
16222
17606
|
}
|
|
16223
17607
|
CanvasManager.CELL_SELECTION_THRESHOLD = 5; // Minimum pixels to drag before cell selection starts
|
|
16224
17608
|
CanvasManager.RELATIVE_DRAG_THRESHOLD = 3; // Minimum pixels to drag before moving starts
|
|
17609
|
+
// Custom text cursor as a black I-beam SVG data URI.
|
|
17610
|
+
// The native 'text' cursor can render as white on Windows browsers,
|
|
17611
|
+
// making it invisible over the white canvas background.
|
|
17612
|
+
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";
|
|
16225
17613
|
|
|
16226
17614
|
/**
|
|
16227
17615
|
* DataBinder handles binding data to documents.
|
|
@@ -16469,8 +17857,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
|
|
|
16469
17857
|
* - Repeating section indicators, loop markers
|
|
16470
17858
|
*/
|
|
16471
17859
|
class PDFGenerator {
|
|
16472
|
-
constructor() {
|
|
17860
|
+
constructor(fontManager) {
|
|
16473
17861
|
this.fontCache = new Map();
|
|
17862
|
+
this.customFontCache = new Map();
|
|
17863
|
+
this.fontManager = fontManager;
|
|
16474
17864
|
}
|
|
16475
17865
|
/**
|
|
16476
17866
|
* Generate a PDF from the document.
|
|
@@ -16481,9 +17871,13 @@ class PDFGenerator {
|
|
|
16481
17871
|
*/
|
|
16482
17872
|
async generate(document, flowedContent, _options) {
|
|
16483
17873
|
const pdfDoc = await pdfLib.PDFDocument.create();
|
|
17874
|
+
pdfDoc.registerFontkit(fontkit);
|
|
16484
17875
|
this.fontCache.clear();
|
|
17876
|
+
this.customFontCache.clear();
|
|
16485
17877
|
// Embed standard fonts we'll need
|
|
16486
17878
|
await this.embedStandardFonts(pdfDoc);
|
|
17879
|
+
// Embed any custom fonts that have font data
|
|
17880
|
+
await this.embedCustomFonts(pdfDoc);
|
|
16487
17881
|
// Render each page
|
|
16488
17882
|
for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
|
|
16489
17883
|
try {
|
|
@@ -16605,11 +17999,59 @@ class PDFGenerator {
|
|
|
16605
17999
|
}
|
|
16606
18000
|
return result;
|
|
16607
18001
|
}
|
|
18002
|
+
/**
|
|
18003
|
+
* Embed custom fonts that have raw font data available.
|
|
18004
|
+
*/
|
|
18005
|
+
async embedCustomFonts(pdfDoc) {
|
|
18006
|
+
const fonts = this.fontManager.getAvailableFonts();
|
|
18007
|
+
for (const font of fonts) {
|
|
18008
|
+
if (font.source !== 'custom')
|
|
18009
|
+
continue;
|
|
18010
|
+
for (const variant of font.variants) {
|
|
18011
|
+
if (!variant.fontData)
|
|
18012
|
+
continue;
|
|
18013
|
+
const cacheKey = `custom:${font.family.toLowerCase()}:${variant.weight}:${variant.style}`;
|
|
18014
|
+
try {
|
|
18015
|
+
// Ensure we pass Uint8Array (some pdf-lib versions need it)
|
|
18016
|
+
const fontBytes = variant.fontData instanceof Uint8Array
|
|
18017
|
+
? variant.fontData
|
|
18018
|
+
: new Uint8Array(variant.fontData);
|
|
18019
|
+
const embedded = await pdfDoc.embedFont(fontBytes, { subset: true });
|
|
18020
|
+
this.customFontCache.set(cacheKey, embedded);
|
|
18021
|
+
Logger.log('[pc-editor:PDFGenerator] Embedded custom font:', font.family, variant.weight, variant.style);
|
|
18022
|
+
}
|
|
18023
|
+
catch (e) {
|
|
18024
|
+
Logger.warn('[pc-editor:PDFGenerator] Failed to embed custom font:', font.family, e);
|
|
18025
|
+
}
|
|
18026
|
+
}
|
|
18027
|
+
}
|
|
18028
|
+
}
|
|
18029
|
+
/**
|
|
18030
|
+
* Check if a font family is a custom font with embedded data.
|
|
18031
|
+
*/
|
|
18032
|
+
isCustomFont(family) {
|
|
18033
|
+
return !this.fontManager.isBuiltIn(family) && this.fontManager.isRegistered(family);
|
|
18034
|
+
}
|
|
16608
18035
|
/**
|
|
16609
18036
|
* Get a font from cache by formatting style.
|
|
18037
|
+
* Checks custom fonts first, then falls back to standard fonts.
|
|
16610
18038
|
*/
|
|
16611
18039
|
getFont(formatting) {
|
|
16612
|
-
const
|
|
18040
|
+
const family = formatting.fontFamily || 'Arial';
|
|
18041
|
+
const weight = formatting.fontWeight || 'normal';
|
|
18042
|
+
const style = formatting.fontStyle || 'normal';
|
|
18043
|
+
// Try custom font first
|
|
18044
|
+
const customKey = `custom:${family.toLowerCase()}:${weight}:${style}`;
|
|
18045
|
+
const customFont = this.customFontCache.get(customKey);
|
|
18046
|
+
if (customFont)
|
|
18047
|
+
return customFont;
|
|
18048
|
+
// Try custom font with normal variant as fallback
|
|
18049
|
+
const customNormalKey = `custom:${family.toLowerCase()}:normal:normal`;
|
|
18050
|
+
const customNormalFont = this.customFontCache.get(customNormalKey);
|
|
18051
|
+
if (customNormalFont)
|
|
18052
|
+
return customNormalFont;
|
|
18053
|
+
// Fall back to standard fonts
|
|
18054
|
+
const standardFont = getStandardFont(family, weight, style);
|
|
16613
18055
|
return this.fontCache.get(standardFont) || this.fontCache.get(pdfLib.StandardFonts.Helvetica);
|
|
16614
18056
|
}
|
|
16615
18057
|
/**
|
|
@@ -16642,12 +18084,14 @@ class PDFGenerator {
|
|
|
16642
18084
|
for (const run of line.runs) {
|
|
16643
18085
|
if (!run.text)
|
|
16644
18086
|
continue;
|
|
16645
|
-
// Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
|
|
16646
|
-
const safeText = this.filterToWinAnsi(run.text);
|
|
16647
|
-
if (!safeText)
|
|
16648
|
-
continue;
|
|
16649
18087
|
// Ensure formatting has required properties with defaults
|
|
16650
18088
|
const formatting = run.formatting || {};
|
|
18089
|
+
// Custom fonts support full Unicode; standard fonts need WinAnsi filtering
|
|
18090
|
+
const safeText = this.isCustomFont(formatting.fontFamily || 'Arial')
|
|
18091
|
+
? run.text
|
|
18092
|
+
: this.filterToWinAnsi(run.text);
|
|
18093
|
+
if (!safeText)
|
|
18094
|
+
continue;
|
|
16651
18095
|
const font = this.getFont(formatting);
|
|
16652
18096
|
const fontSize = formatting.fontSize || 14;
|
|
16653
18097
|
const color = parseColor(formatting.color || '#000000');
|
|
@@ -21322,6 +22766,156 @@ class PDFImporter {
|
|
|
21322
22766
|
}
|
|
21323
22767
|
}
|
|
21324
22768
|
|
|
22769
|
+
/**
|
|
22770
|
+
* FontManager - Manages font registration and availability for the editor.
|
|
22771
|
+
*
|
|
22772
|
+
* Built-in fonts are web-safe and map to pdf-lib StandardFonts.
|
|
22773
|
+
* Custom fonts are loaded via the FontFace API for canvas rendering
|
|
22774
|
+
* and their raw bytes are stored for PDF embedding.
|
|
22775
|
+
*/
|
|
22776
|
+
/**
|
|
22777
|
+
* Built-in web-safe fonts that need no loading.
|
|
22778
|
+
*/
|
|
22779
|
+
const BUILT_IN_FONTS = [
|
|
22780
|
+
'Arial',
|
|
22781
|
+
'Times New Roman',
|
|
22782
|
+
'Courier New',
|
|
22783
|
+
'Georgia',
|
|
22784
|
+
'Verdana'
|
|
22785
|
+
];
|
|
22786
|
+
class FontManager extends EventEmitter {
|
|
22787
|
+
constructor() {
|
|
22788
|
+
super();
|
|
22789
|
+
this.fonts = new Map();
|
|
22790
|
+
// Register built-in fonts
|
|
22791
|
+
for (const family of BUILT_IN_FONTS) {
|
|
22792
|
+
this.fonts.set(family.toLowerCase(), {
|
|
22793
|
+
family,
|
|
22794
|
+
source: 'built-in',
|
|
22795
|
+
variants: [{
|
|
22796
|
+
weight: 'normal',
|
|
22797
|
+
style: 'normal',
|
|
22798
|
+
fontData: null,
|
|
22799
|
+
loaded: true
|
|
22800
|
+
}]
|
|
22801
|
+
});
|
|
22802
|
+
}
|
|
22803
|
+
}
|
|
22804
|
+
/**
|
|
22805
|
+
* Register a custom font. Fetches the font data if a URL is provided,
|
|
22806
|
+
* creates a FontFace for canvas rendering, and stores the raw bytes for PDF embedding.
|
|
22807
|
+
*/
|
|
22808
|
+
async registerFont(options) {
|
|
22809
|
+
const { family, url, data, weight = 'normal', style = 'normal' } = options;
|
|
22810
|
+
Logger.log('[pc-editor:FontManager] registerFont', family, weight, style);
|
|
22811
|
+
let fontData = null;
|
|
22812
|
+
// Get font bytes
|
|
22813
|
+
if (data) {
|
|
22814
|
+
fontData = data;
|
|
22815
|
+
}
|
|
22816
|
+
else if (url) {
|
|
22817
|
+
try {
|
|
22818
|
+
const response = await fetch(url);
|
|
22819
|
+
if (!response.ok) {
|
|
22820
|
+
throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`);
|
|
22821
|
+
}
|
|
22822
|
+
fontData = await response.arrayBuffer();
|
|
22823
|
+
}
|
|
22824
|
+
catch (e) {
|
|
22825
|
+
Logger.error(`[pc-editor:FontManager] Failed to fetch font "${family}" from ${url}:`, e);
|
|
22826
|
+
throw e;
|
|
22827
|
+
}
|
|
22828
|
+
}
|
|
22829
|
+
// Create FontFace for canvas rendering
|
|
22830
|
+
if (fontData && typeof FontFace !== 'undefined') {
|
|
22831
|
+
try {
|
|
22832
|
+
const fontFace = new FontFace(family, fontData, {
|
|
22833
|
+
weight,
|
|
22834
|
+
style
|
|
22835
|
+
});
|
|
22836
|
+
await fontFace.load();
|
|
22837
|
+
document.fonts.add(fontFace);
|
|
22838
|
+
Logger.log('[pc-editor:FontManager] FontFace loaded:', family, weight, style);
|
|
22839
|
+
}
|
|
22840
|
+
catch (e) {
|
|
22841
|
+
Logger.error(`[pc-editor:FontManager] Failed to load FontFace "${family}":`, e);
|
|
22842
|
+
throw e;
|
|
22843
|
+
}
|
|
22844
|
+
}
|
|
22845
|
+
// Register in our map
|
|
22846
|
+
const key = family.toLowerCase();
|
|
22847
|
+
let registration = this.fonts.get(key);
|
|
22848
|
+
if (!registration) {
|
|
22849
|
+
registration = {
|
|
22850
|
+
family,
|
|
22851
|
+
source: 'custom',
|
|
22852
|
+
variants: []
|
|
22853
|
+
};
|
|
22854
|
+
this.fonts.set(key, registration);
|
|
22855
|
+
}
|
|
22856
|
+
else if (registration.source === 'built-in') {
|
|
22857
|
+
// Upgrading a built-in font with custom data (e.g., for PDF embedding)
|
|
22858
|
+
registration.source = 'custom';
|
|
22859
|
+
}
|
|
22860
|
+
// Add or update variant
|
|
22861
|
+
const existingVariant = registration.variants.find(v => v.weight === weight && v.style === style);
|
|
22862
|
+
if (existingVariant) {
|
|
22863
|
+
existingVariant.fontData = fontData;
|
|
22864
|
+
existingVariant.loaded = true;
|
|
22865
|
+
}
|
|
22866
|
+
else {
|
|
22867
|
+
registration.variants.push({
|
|
22868
|
+
weight,
|
|
22869
|
+
style,
|
|
22870
|
+
fontData,
|
|
22871
|
+
loaded: true
|
|
22872
|
+
});
|
|
22873
|
+
}
|
|
22874
|
+
this.emit('font-registered', { family, weight, style });
|
|
22875
|
+
}
|
|
22876
|
+
/**
|
|
22877
|
+
* Get all registered font families.
|
|
22878
|
+
*/
|
|
22879
|
+
getAvailableFonts() {
|
|
22880
|
+
return Array.from(this.fonts.values());
|
|
22881
|
+
}
|
|
22882
|
+
/**
|
|
22883
|
+
* Get all available font family names.
|
|
22884
|
+
*/
|
|
22885
|
+
getAvailableFontFamilies() {
|
|
22886
|
+
return Array.from(this.fonts.values()).map(f => f.family);
|
|
22887
|
+
}
|
|
22888
|
+
/**
|
|
22889
|
+
* Check if a font family is built-in.
|
|
22890
|
+
*/
|
|
22891
|
+
isBuiltIn(family) {
|
|
22892
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22893
|
+
return reg?.source === 'built-in';
|
|
22894
|
+
}
|
|
22895
|
+
/**
|
|
22896
|
+
* Check if a font family is registered (built-in or custom).
|
|
22897
|
+
*/
|
|
22898
|
+
isRegistered(family) {
|
|
22899
|
+
return this.fonts.has(family.toLowerCase());
|
|
22900
|
+
}
|
|
22901
|
+
/**
|
|
22902
|
+
* Get raw font bytes for PDF embedding.
|
|
22903
|
+
* Returns null for built-in fonts (they use StandardFonts in pdf-lib).
|
|
22904
|
+
*/
|
|
22905
|
+
getFontData(family, weight = 'normal', style = 'normal') {
|
|
22906
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22907
|
+
if (!reg)
|
|
22908
|
+
return null;
|
|
22909
|
+
// Try exact match first
|
|
22910
|
+
const exact = reg.variants.find(v => v.weight === weight && v.style === style);
|
|
22911
|
+
if (exact?.fontData)
|
|
22912
|
+
return exact.fontData;
|
|
22913
|
+
// Fall back to normal variant
|
|
22914
|
+
const normal = reg.variants.find(v => v.weight === 'normal' && v.style === 'normal');
|
|
22915
|
+
return normal?.fontData || null;
|
|
22916
|
+
}
|
|
22917
|
+
}
|
|
22918
|
+
|
|
21325
22919
|
class PCEditor extends EventEmitter {
|
|
21326
22920
|
constructor(container, options) {
|
|
21327
22921
|
super();
|
|
@@ -21348,7 +22942,8 @@ class PCEditor extends EventEmitter {
|
|
|
21348
22942
|
units: this.options.units
|
|
21349
22943
|
});
|
|
21350
22944
|
this.dataBinder = new DataBinder();
|
|
21351
|
-
this.
|
|
22945
|
+
this.fontManager = new FontManager();
|
|
22946
|
+
this.pdfGenerator = new PDFGenerator(this.fontManager);
|
|
21352
22947
|
this.clipboardManager = new ClipboardManager();
|
|
21353
22948
|
this.initialize();
|
|
21354
22949
|
}
|
|
@@ -21510,6 +23105,14 @@ class PCEditor extends EventEmitter {
|
|
|
21510
23105
|
this.canvasManager.on('tablecell-cursor-changed', (data) => {
|
|
21511
23106
|
this.emit('tablecell-cursor-changed', data);
|
|
21512
23107
|
});
|
|
23108
|
+
// Forward table cell selection changes (multi-cell drag/shift-click)
|
|
23109
|
+
this.canvasManager.on('table-cell-selection-changed', (data) => {
|
|
23110
|
+
this.emit('table-cell-selection-changed', data);
|
|
23111
|
+
});
|
|
23112
|
+
// Forward table row loop clicks
|
|
23113
|
+
this.canvasManager.on('table-row-loop-clicked', (data) => {
|
|
23114
|
+
this.emit('table-row-loop-clicked', data);
|
|
23115
|
+
});
|
|
21513
23116
|
this.canvasManager.on('repeating-section-clicked', (data) => {
|
|
21514
23117
|
// Repeating section clicked - update selection state
|
|
21515
23118
|
if (data.section && data.section.id) {
|
|
@@ -21520,6 +23123,16 @@ class PCEditor extends EventEmitter {
|
|
|
21520
23123
|
this.emitSelectionChange();
|
|
21521
23124
|
}
|
|
21522
23125
|
});
|
|
23126
|
+
this.canvasManager.on('conditional-section-clicked', (data) => {
|
|
23127
|
+
// Conditional section clicked - update selection state
|
|
23128
|
+
if (data.section && data.section.id) {
|
|
23129
|
+
this.currentSelection = {
|
|
23130
|
+
type: 'conditional-section',
|
|
23131
|
+
sectionId: data.section.id
|
|
23132
|
+
};
|
|
23133
|
+
this.emitSelectionChange();
|
|
23134
|
+
}
|
|
23135
|
+
});
|
|
21523
23136
|
// Listen for section focus changes from CanvasManager (double-click)
|
|
21524
23137
|
this.canvasManager.on('section-focus-changed', (data) => {
|
|
21525
23138
|
// Update our internal state to match the canvas manager
|
|
@@ -22348,17 +23961,24 @@ class PCEditor extends EventEmitter {
|
|
|
22348
23961
|
this.selectAll();
|
|
22349
23962
|
return;
|
|
22350
23963
|
}
|
|
22351
|
-
// If an embedded object is selected (but not being edited),
|
|
22352
|
-
|
|
22353
|
-
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
22354
|
-
if (isArrowKey && this.canvasManager.hasSelectedElements()) {
|
|
22355
|
-
// Check if we're not in editing mode
|
|
23964
|
+
// If an embedded object is selected (but not being edited), handle special keys
|
|
23965
|
+
if (this.canvasManager.hasSelectedElements()) {
|
|
22356
23966
|
const editingTextBox = this.canvasManager.getEditingTextBox();
|
|
22357
23967
|
const focusedTable = this.canvasManager.getFocusedControl();
|
|
22358
23968
|
const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
|
|
22359
23969
|
if (!isEditing) {
|
|
22360
|
-
//
|
|
22361
|
-
|
|
23970
|
+
// Arrow keys: deselect and move cursor in text flow
|
|
23971
|
+
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
23972
|
+
if (isArrowKey) {
|
|
23973
|
+
this.canvasManager.clearSelection();
|
|
23974
|
+
// Fall through to normal key handling
|
|
23975
|
+
}
|
|
23976
|
+
// Backspace/Delete: delete the selected object from the text flow
|
|
23977
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
23978
|
+
e.preventDefault();
|
|
23979
|
+
this.deleteSelectedElements();
|
|
23980
|
+
return;
|
|
23981
|
+
}
|
|
22362
23982
|
}
|
|
22363
23983
|
}
|
|
22364
23984
|
// Use the unified focus system to get the currently focused control
|
|
@@ -22461,6 +24081,32 @@ class PCEditor extends EventEmitter {
|
|
|
22461
24081
|
this.canvasManager.clearSelection();
|
|
22462
24082
|
this.canvasManager.render();
|
|
22463
24083
|
}
|
|
24084
|
+
/**
|
|
24085
|
+
* Delete all currently selected embedded objects from the text flow.
|
|
24086
|
+
*/
|
|
24087
|
+
deleteSelectedElements() {
|
|
24088
|
+
const selectedElements = this.canvasManager.getSelectedElements();
|
|
24089
|
+
if (selectedElements.length === 0)
|
|
24090
|
+
return;
|
|
24091
|
+
for (const elementId of selectedElements) {
|
|
24092
|
+
const objectInfo = this.findEmbeddedObjectInfo(elementId);
|
|
24093
|
+
if (objectInfo) {
|
|
24094
|
+
// Delete the placeholder character at the object's text index
|
|
24095
|
+
// This removes the object from the text flow
|
|
24096
|
+
objectInfo.content.deleteText(objectInfo.textIndex, 1);
|
|
24097
|
+
// Return focus to the parent flowing content
|
|
24098
|
+
const cursorPos = Math.min(objectInfo.textIndex, objectInfo.content.getText().length);
|
|
24099
|
+
objectInfo.content.setCursorPosition(cursorPos);
|
|
24100
|
+
this.canvasManager.setFocus(objectInfo.content);
|
|
24101
|
+
if (objectInfo.section !== this.canvasManager.getActiveSection()) {
|
|
24102
|
+
this.canvasManager.setActiveSection(objectInfo.section);
|
|
24103
|
+
}
|
|
24104
|
+
}
|
|
24105
|
+
}
|
|
24106
|
+
this.canvasManager.clearSelection();
|
|
24107
|
+
this.canvasManager.render();
|
|
24108
|
+
this.emit('content-changed', {});
|
|
24109
|
+
}
|
|
22464
24110
|
/**
|
|
22465
24111
|
* Find embedded object info by ID across all flowing content sources.
|
|
22466
24112
|
*/
|
|
@@ -23479,6 +25125,39 @@ class PCEditor extends EventEmitter {
|
|
|
23479
25125
|
table.removeColumn(colIndex);
|
|
23480
25126
|
this.canvasManager.render();
|
|
23481
25127
|
}
|
|
25128
|
+
/**
|
|
25129
|
+
* Merge selected cells in a table.
|
|
25130
|
+
* Uses the table's current cell selection range.
|
|
25131
|
+
* @param table The table containing the cells to merge
|
|
25132
|
+
* @returns true if cells were merged successfully
|
|
25133
|
+
*/
|
|
25134
|
+
tableMergeCells(table) {
|
|
25135
|
+
Logger.log('[pc-editor] tableMergeCells');
|
|
25136
|
+
if (!this._isReady)
|
|
25137
|
+
return false;
|
|
25138
|
+
const result = table.mergeCells();
|
|
25139
|
+
if (result.success) {
|
|
25140
|
+
this.canvasManager.render();
|
|
25141
|
+
}
|
|
25142
|
+
return result.success;
|
|
25143
|
+
}
|
|
25144
|
+
/**
|
|
25145
|
+
* Split a merged cell back into individual cells.
|
|
25146
|
+
* @param table The table containing the merged cell
|
|
25147
|
+
* @param row Row index of the merged cell
|
|
25148
|
+
* @param col Column index of the merged cell
|
|
25149
|
+
* @returns true if the cell was split successfully
|
|
25150
|
+
*/
|
|
25151
|
+
tableSplitCell(table, row, col) {
|
|
25152
|
+
Logger.log('[pc-editor] tableSplitCell', row, col);
|
|
25153
|
+
if (!this._isReady)
|
|
25154
|
+
return false;
|
|
25155
|
+
const result = table.splitCell(row, col);
|
|
25156
|
+
if (result.success) {
|
|
25157
|
+
this.canvasManager.render();
|
|
25158
|
+
}
|
|
25159
|
+
return result.success;
|
|
25160
|
+
}
|
|
23482
25161
|
/**
|
|
23483
25162
|
* Begin a compound operation. Groups multiple mutations into a single undo entry.
|
|
23484
25163
|
* Call endCompoundOperation after making changes.
|
|
@@ -23545,11 +25224,17 @@ class PCEditor extends EventEmitter {
|
|
|
23545
25224
|
let totalFieldCount = 0;
|
|
23546
25225
|
// Step 1: Expand repeating sections in body (header/footer don't support them)
|
|
23547
25226
|
this.expandRepeatingSections(bodyContent, data);
|
|
23548
|
-
// Step 2:
|
|
25227
|
+
// Step 2: Evaluate conditional sections in body (remove content where predicate is false)
|
|
25228
|
+
this.evaluateConditionalSections(bodyContent, data);
|
|
25229
|
+
// Step 3: Expand table row loops in body, header, and footer
|
|
23549
25230
|
this.expandTableRowLoops(bodyContent, data);
|
|
23550
25231
|
this.expandTableRowLoops(this.document.headerFlowingContent, data);
|
|
23551
25232
|
this.expandTableRowLoops(this.document.footerFlowingContent, data);
|
|
23552
|
-
// Step
|
|
25233
|
+
// Step 4: Evaluate table row conditionals in body, header, and footer
|
|
25234
|
+
this.evaluateTableRowConditionals(bodyContent, data);
|
|
25235
|
+
this.evaluateTableRowConditionals(this.document.headerFlowingContent, data);
|
|
25236
|
+
this.evaluateTableRowConditionals(this.document.footerFlowingContent, data);
|
|
25237
|
+
// Step 5: Substitute all fields in body
|
|
23553
25238
|
totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
|
|
23554
25239
|
// Step 4: Substitute all fields in embedded objects in body
|
|
23555
25240
|
totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
|
|
@@ -23757,9 +25442,62 @@ class PCEditor extends EventEmitter {
|
|
|
23757
25442
|
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
23758
25443
|
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
23759
25444
|
}
|
|
23760
|
-
// Remove the section after expansion
|
|
23761
|
-
sectionManager.remove(section.id);
|
|
25445
|
+
// Remove the section after expansion
|
|
25446
|
+
sectionManager.remove(section.id);
|
|
25447
|
+
}
|
|
25448
|
+
}
|
|
25449
|
+
/**
|
|
25450
|
+
* Evaluate conditional sections by removing content where predicate is false.
|
|
25451
|
+
* Processes sections from end to start to preserve text indices.
|
|
25452
|
+
*/
|
|
25453
|
+
evaluateConditionalSections(flowingContent, data) {
|
|
25454
|
+
const sectionManager = flowingContent.getConditionalSectionManager();
|
|
25455
|
+
// Get sections in descending order (process end-to-start)
|
|
25456
|
+
const sections = sectionManager.getSectionsDescending();
|
|
25457
|
+
for (const section of sections) {
|
|
25458
|
+
const result = PredicateEvaluator.evaluate(section.predicate, data);
|
|
25459
|
+
if (!result) {
|
|
25460
|
+
// Predicate is false — remove the content within this section
|
|
25461
|
+
const deleteStart = section.startIndex;
|
|
25462
|
+
const deleteLength = section.endIndex - section.startIndex;
|
|
25463
|
+
flowingContent.deleteText(deleteStart, deleteLength);
|
|
25464
|
+
}
|
|
25465
|
+
// Remove the conditional section marker regardless
|
|
25466
|
+
sectionManager.remove(section.id);
|
|
25467
|
+
}
|
|
25468
|
+
}
|
|
25469
|
+
/**
|
|
25470
|
+
* Evaluate table row conditionals in embedded tables within a FlowingTextContent.
|
|
25471
|
+
* For each table with row conditionals, removes rows where predicate is false.
|
|
25472
|
+
*/
|
|
25473
|
+
evaluateTableRowConditionals(flowingContent, data) {
|
|
25474
|
+
const embeddedObjects = flowingContent.getEmbeddedObjects();
|
|
25475
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
25476
|
+
if (obj instanceof TableObject) {
|
|
25477
|
+
this.evaluateTableRowConditionalsInTable(obj, data);
|
|
25478
|
+
}
|
|
25479
|
+
}
|
|
25480
|
+
}
|
|
25481
|
+
/**
|
|
25482
|
+
* Evaluate row conditionals in a single table.
|
|
25483
|
+
* Processes conditionals from end to start to preserve row indices.
|
|
25484
|
+
*/
|
|
25485
|
+
evaluateTableRowConditionalsInTable(table, data) {
|
|
25486
|
+
const conditionals = table.getAllRowConditionals();
|
|
25487
|
+
if (conditionals.length === 0)
|
|
25488
|
+
return;
|
|
25489
|
+
// Sort by startRowIndex descending (process end-to-start)
|
|
25490
|
+
const sorted = [...conditionals].sort((a, b) => b.startRowIndex - a.startRowIndex);
|
|
25491
|
+
for (const cond of sorted) {
|
|
25492
|
+
const result = PredicateEvaluator.evaluate(cond.predicate, data);
|
|
25493
|
+
if (!result) {
|
|
25494
|
+
// Predicate is false — remove the rows
|
|
25495
|
+
table.removeRowsInRange(cond.startRowIndex, cond.endRowIndex);
|
|
25496
|
+
}
|
|
25497
|
+
// Remove the conditional marker regardless
|
|
25498
|
+
table.removeRowConditional(cond.id);
|
|
23762
25499
|
}
|
|
25500
|
+
table.markLayoutDirty();
|
|
23763
25501
|
}
|
|
23764
25502
|
/**
|
|
23765
25503
|
* Get a value at a path without array defaulting.
|
|
@@ -24008,7 +25746,7 @@ class PCEditor extends EventEmitter {
|
|
|
24008
25746
|
toggleBulletList() {
|
|
24009
25747
|
if (!this._isReady)
|
|
24010
25748
|
return;
|
|
24011
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25749
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24012
25750
|
if (!flowingContent)
|
|
24013
25751
|
return;
|
|
24014
25752
|
flowingContent.toggleBulletList();
|
|
@@ -24021,7 +25759,7 @@ class PCEditor extends EventEmitter {
|
|
|
24021
25759
|
toggleNumberedList() {
|
|
24022
25760
|
if (!this._isReady)
|
|
24023
25761
|
return;
|
|
24024
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25762
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24025
25763
|
if (!flowingContent)
|
|
24026
25764
|
return;
|
|
24027
25765
|
flowingContent.toggleNumberedList();
|
|
@@ -24034,7 +25772,7 @@ class PCEditor extends EventEmitter {
|
|
|
24034
25772
|
indentParagraph() {
|
|
24035
25773
|
if (!this._isReady)
|
|
24036
25774
|
return;
|
|
24037
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25775
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24038
25776
|
if (!flowingContent)
|
|
24039
25777
|
return;
|
|
24040
25778
|
flowingContent.indentParagraph();
|
|
@@ -24047,7 +25785,7 @@ class PCEditor extends EventEmitter {
|
|
|
24047
25785
|
outdentParagraph() {
|
|
24048
25786
|
if (!this._isReady)
|
|
24049
25787
|
return;
|
|
24050
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25788
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24051
25789
|
if (!flowingContent)
|
|
24052
25790
|
return;
|
|
24053
25791
|
flowingContent.outdentParagraph();
|
|
@@ -24060,7 +25798,7 @@ class PCEditor extends EventEmitter {
|
|
|
24060
25798
|
getListFormatting() {
|
|
24061
25799
|
if (!this._isReady)
|
|
24062
25800
|
return undefined;
|
|
24063
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25801
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24064
25802
|
if (!flowingContent)
|
|
24065
25803
|
return undefined;
|
|
24066
25804
|
return flowingContent.getListFormatting();
|
|
@@ -24271,9 +26009,12 @@ class PCEditor extends EventEmitter {
|
|
|
24271
26009
|
// If a table is focused, create a row loop instead of a text repeating section
|
|
24272
26010
|
const focusedTable = this.getFocusedTable();
|
|
24273
26011
|
if (focusedTable && focusedTable.focusedCell) {
|
|
24274
|
-
|
|
24275
|
-
const
|
|
24276
|
-
const
|
|
26012
|
+
// Use the selected range if multiple rows are selected, otherwise use the focused cell's row
|
|
26013
|
+
const selectedRange = focusedTable.selectedRange;
|
|
26014
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
26015
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
26016
|
+
Logger.log('[pc-editor] createRepeatingSection → table row loop', startRow, endRow, fieldPath);
|
|
26017
|
+
const loop = focusedTable.createRowLoop(startRow, endRow, fieldPath);
|
|
24277
26018
|
if (loop) {
|
|
24278
26019
|
this.canvasManager.render();
|
|
24279
26020
|
this.emit('table-row-loop-added', { table: focusedTable, loop });
|
|
@@ -24345,6 +26086,103 @@ class PCEditor extends EventEmitter {
|
|
|
24345
26086
|
return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
|
|
24346
26087
|
}
|
|
24347
26088
|
// ============================================
|
|
26089
|
+
// Conditional Section API
|
|
26090
|
+
// ============================================
|
|
26091
|
+
/**
|
|
26092
|
+
* Create a conditional section.
|
|
26093
|
+
*
|
|
26094
|
+
* If a table is currently being edited (focused), creates a table row conditional
|
|
26095
|
+
* based on the focused cell's row.
|
|
26096
|
+
*
|
|
26097
|
+
* Otherwise, creates a body text conditional section at the given paragraph boundaries.
|
|
26098
|
+
*
|
|
26099
|
+
* @param startIndex Text index at paragraph start (ignored for table row conditionals)
|
|
26100
|
+
* @param endIndex Text index at closing paragraph start (ignored for table row conditionals)
|
|
26101
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
26102
|
+
* @returns The created section, or null if creation failed
|
|
26103
|
+
*/
|
|
26104
|
+
addConditionalSection(startIndex, endIndex, predicate) {
|
|
26105
|
+
if (!this._isReady) {
|
|
26106
|
+
throw new Error('Editor is not ready');
|
|
26107
|
+
}
|
|
26108
|
+
// If a table is focused, create a row conditional instead
|
|
26109
|
+
const focusedTable = this.getFocusedTable();
|
|
26110
|
+
if (focusedTable && focusedTable.focusedCell) {
|
|
26111
|
+
const selectedRange = focusedTable.selectedRange;
|
|
26112
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
26113
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
26114
|
+
Logger.log('[pc-editor] addConditionalSection → table row conditional', startRow, endRow, predicate);
|
|
26115
|
+
const cond = focusedTable.createRowConditional(startRow, endRow, predicate);
|
|
26116
|
+
if (cond) {
|
|
26117
|
+
this.canvasManager.render();
|
|
26118
|
+
this.emit('table-row-conditional-added', { table: focusedTable, conditional: cond });
|
|
26119
|
+
}
|
|
26120
|
+
return null; // Row conditionals are not ConditionalSections, return null
|
|
26121
|
+
}
|
|
26122
|
+
Logger.log('[pc-editor] addConditionalSection', startIndex, endIndex, predicate);
|
|
26123
|
+
const section = this.document.bodyFlowingContent.createConditionalSection(startIndex, endIndex, predicate);
|
|
26124
|
+
if (section) {
|
|
26125
|
+
this.canvasManager.render();
|
|
26126
|
+
this.emit('conditional-section-added', { section });
|
|
26127
|
+
}
|
|
26128
|
+
return section;
|
|
26129
|
+
}
|
|
26130
|
+
/**
|
|
26131
|
+
* Get a conditional section by ID.
|
|
26132
|
+
*/
|
|
26133
|
+
getConditionalSection(id) {
|
|
26134
|
+
if (!this._isReady) {
|
|
26135
|
+
return null;
|
|
26136
|
+
}
|
|
26137
|
+
return this.document.bodyFlowingContent.getConditionalSection(id) || null;
|
|
26138
|
+
}
|
|
26139
|
+
/**
|
|
26140
|
+
* Get all conditional sections.
|
|
26141
|
+
*/
|
|
26142
|
+
getConditionalSections() {
|
|
26143
|
+
if (!this._isReady) {
|
|
26144
|
+
return [];
|
|
26145
|
+
}
|
|
26146
|
+
return this.document.bodyFlowingContent.getConditionalSections();
|
|
26147
|
+
}
|
|
26148
|
+
/**
|
|
26149
|
+
* Update a conditional section's predicate.
|
|
26150
|
+
*/
|
|
26151
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
26152
|
+
if (!this._isReady) {
|
|
26153
|
+
return false;
|
|
26154
|
+
}
|
|
26155
|
+
const success = this.document.bodyFlowingContent.updateConditionalSectionPredicate(id, predicate);
|
|
26156
|
+
if (success) {
|
|
26157
|
+
this.canvasManager.render();
|
|
26158
|
+
this.emit('conditional-section-updated', { id, predicate });
|
|
26159
|
+
}
|
|
26160
|
+
return success;
|
|
26161
|
+
}
|
|
26162
|
+
/**
|
|
26163
|
+
* Remove a conditional section by ID.
|
|
26164
|
+
*/
|
|
26165
|
+
removeConditionalSection(id) {
|
|
26166
|
+
if (!this._isReady) {
|
|
26167
|
+
return false;
|
|
26168
|
+
}
|
|
26169
|
+
const success = this.document.bodyFlowingContent.removeConditionalSection(id);
|
|
26170
|
+
if (success) {
|
|
26171
|
+
this.canvasManager.render();
|
|
26172
|
+
this.emit('conditional-section-removed', { id });
|
|
26173
|
+
}
|
|
26174
|
+
return success;
|
|
26175
|
+
}
|
|
26176
|
+
/**
|
|
26177
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
26178
|
+
*/
|
|
26179
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
26180
|
+
if (!this._isReady) {
|
|
26181
|
+
return null;
|
|
26182
|
+
}
|
|
26183
|
+
return this.document.bodyFlowingContent.getConditionalSectionAtBoundary(textIndex) || null;
|
|
26184
|
+
}
|
|
26185
|
+
// ============================================
|
|
24348
26186
|
// Header/Footer API
|
|
24349
26187
|
// ============================================
|
|
24350
26188
|
/**
|
|
@@ -24695,6 +26533,39 @@ class PCEditor extends EventEmitter {
|
|
|
24695
26533
|
setLogging(enabled) {
|
|
24696
26534
|
Logger.setEnabled(enabled);
|
|
24697
26535
|
}
|
|
26536
|
+
// ============================================
|
|
26537
|
+
// Font Management
|
|
26538
|
+
// ============================================
|
|
26539
|
+
/**
|
|
26540
|
+
* Register a custom font for use in the editor and PDF export.
|
|
26541
|
+
* The font will be loaded via the FontFace API for canvas rendering
|
|
26542
|
+
* and its raw bytes stored for PDF embedding.
|
|
26543
|
+
* @param options Font registration options (family + url or data)
|
|
26544
|
+
*/
|
|
26545
|
+
async registerFont(options) {
|
|
26546
|
+
Logger.log('[pc-editor] registerFont', options.family);
|
|
26547
|
+
await this.fontManager.registerFont(options);
|
|
26548
|
+
this.emit('font-registered', { family: options.family });
|
|
26549
|
+
// Re-render to pick up the new font if it's already in use
|
|
26550
|
+
if (this._isReady) {
|
|
26551
|
+
this.canvasManager.render();
|
|
26552
|
+
}
|
|
26553
|
+
}
|
|
26554
|
+
/**
|
|
26555
|
+
* Get all registered fonts (built-in and custom).
|
|
26556
|
+
*/
|
|
26557
|
+
getAvailableFonts() {
|
|
26558
|
+
return this.fontManager.getAvailableFonts().map(f => ({
|
|
26559
|
+
family: f.family,
|
|
26560
|
+
source: f.source
|
|
26561
|
+
}));
|
|
26562
|
+
}
|
|
26563
|
+
/**
|
|
26564
|
+
* Get all available font family names.
|
|
26565
|
+
*/
|
|
26566
|
+
getAvailableFontFamilies() {
|
|
26567
|
+
return this.fontManager.getAvailableFontFamilies();
|
|
26568
|
+
}
|
|
24698
26569
|
destroy() {
|
|
24699
26570
|
this.disableTextInput();
|
|
24700
26571
|
if (this.canvasManager) {
|
|
@@ -26211,7 +28082,7 @@ class MergeDataPane extends BasePane {
|
|
|
26211
28082
|
createContent() {
|
|
26212
28083
|
const container = document.createElement('div');
|
|
26213
28084
|
// Textarea for JSON
|
|
26214
|
-
const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
|
|
28085
|
+
const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
|
|
26215
28086
|
container.appendChild(textareaGroup);
|
|
26216
28087
|
// Error hint (hidden by default)
|
|
26217
28088
|
this.errorHint = this.createHint('');
|
|
@@ -26357,17 +28228,29 @@ class FormattingPane extends BasePane {
|
|
|
26357
28228
|
attach(options) {
|
|
26358
28229
|
super.attach(options);
|
|
26359
28230
|
if (this.editor) {
|
|
28231
|
+
// Populate font list from editor if no explicit list was provided
|
|
28232
|
+
if (this.fontFamilies === DEFAULT_FONT_FAMILIES) {
|
|
28233
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28234
|
+
this.rebuildFontSelect();
|
|
28235
|
+
}
|
|
26360
28236
|
// Update on cursor/selection changes
|
|
26361
28237
|
const updateHandler = () => this.updateFromEditor();
|
|
26362
28238
|
this.editor.on('cursor-changed', updateHandler);
|
|
26363
28239
|
this.editor.on('selection-changed', updateHandler);
|
|
26364
28240
|
this.editor.on('text-changed', updateHandler);
|
|
26365
28241
|
this.editor.on('formatting-changed', updateHandler);
|
|
28242
|
+
// Update font list when new fonts are registered
|
|
28243
|
+
const fontHandler = () => {
|
|
28244
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28245
|
+
this.rebuildFontSelect();
|
|
28246
|
+
};
|
|
28247
|
+
this.editor.on('font-registered', fontHandler);
|
|
26366
28248
|
this.eventCleanup.push(() => {
|
|
26367
28249
|
this.editor?.off('cursor-changed', updateHandler);
|
|
26368
28250
|
this.editor?.off('selection-changed', updateHandler);
|
|
26369
28251
|
this.editor?.off('text-changed', updateHandler);
|
|
26370
28252
|
this.editor?.off('formatting-changed', updateHandler);
|
|
28253
|
+
this.editor?.off('font-registered', fontHandler);
|
|
26371
28254
|
});
|
|
26372
28255
|
// Initial update
|
|
26373
28256
|
this.updateFromEditor();
|
|
@@ -26436,38 +28319,82 @@ class FormattingPane extends BasePane {
|
|
|
26436
28319
|
listsGroup.appendChild(this.outdentBtn);
|
|
26437
28320
|
listsSection.appendChild(listsGroup);
|
|
26438
28321
|
container.appendChild(listsSection);
|
|
26439
|
-
// Font section
|
|
28322
|
+
// Font section - label-value grid with right-aligned labels
|
|
26440
28323
|
const fontSection = this.createSection('Font');
|
|
28324
|
+
const fontGrid = document.createElement('div');
|
|
28325
|
+
fontGrid.className = 'pc-pane-label-value-grid';
|
|
28326
|
+
// Family row
|
|
28327
|
+
const familyLabel = document.createElement('label');
|
|
28328
|
+
familyLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28329
|
+
familyLabel.textContent = 'Family:';
|
|
26441
28330
|
this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
|
|
26442
28331
|
this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
|
|
26443
|
-
|
|
28332
|
+
fontGrid.appendChild(familyLabel);
|
|
28333
|
+
fontGrid.appendChild(this.fontFamilySelect);
|
|
28334
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28335
|
+
// Size row
|
|
28336
|
+
const sizeLabel = document.createElement('label');
|
|
28337
|
+
sizeLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28338
|
+
sizeLabel.textContent = 'Size:';
|
|
26444
28339
|
this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
|
|
26445
28340
|
this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
|
|
26446
|
-
|
|
28341
|
+
fontGrid.appendChild(sizeLabel);
|
|
28342
|
+
fontGrid.appendChild(this.fontSizeSelect);
|
|
28343
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28344
|
+
fontSection.appendChild(fontGrid);
|
|
26447
28345
|
container.appendChild(fontSection);
|
|
26448
|
-
// Color section
|
|
28346
|
+
// Color section - label-value grid with right-aligned labels
|
|
26449
28347
|
const colorSection = this.createSection('Color');
|
|
26450
|
-
const
|
|
26451
|
-
|
|
28348
|
+
const colorGrid = document.createElement('div');
|
|
28349
|
+
colorGrid.className = 'pc-pane-label-value-grid';
|
|
28350
|
+
// Text color row: label | picker | spacer
|
|
28351
|
+
const textColorLabel = document.createElement('label');
|
|
28352
|
+
textColorLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28353
|
+
textColorLabel.textContent = 'Text:';
|
|
26452
28354
|
this.colorInput = this.createColorInput('#000000');
|
|
26453
28355
|
this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
28356
|
+
colorGrid.appendChild(textColorLabel);
|
|
28357
|
+
colorGrid.appendChild(this.colorInput);
|
|
28358
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28359
|
+
// Highlight row: label | picker + clear button | spacer
|
|
28360
|
+
const highlightLabel = document.createElement('label');
|
|
28361
|
+
highlightLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28362
|
+
highlightLabel.textContent = 'Highlight:';
|
|
26457
28363
|
this.highlightInput = this.createColorInput('#ffff00');
|
|
26458
28364
|
this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
|
|
26459
|
-
const
|
|
28365
|
+
const highlightControls = document.createElement('div');
|
|
28366
|
+
highlightControls.style.display = 'flex';
|
|
28367
|
+
highlightControls.style.alignItems = 'center';
|
|
28368
|
+
highlightControls.style.gap = '4px';
|
|
28369
|
+
highlightControls.appendChild(this.highlightInput);
|
|
26460
28370
|
const clearHighlightBtn = this.createButton('Clear');
|
|
26461
28371
|
clearHighlightBtn.className = 'pc-pane-button';
|
|
26462
|
-
clearHighlightBtn.style.marginLeft = '4px';
|
|
26463
28372
|
this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
26467
|
-
|
|
28373
|
+
highlightControls.appendChild(clearHighlightBtn);
|
|
28374
|
+
colorGrid.appendChild(highlightLabel);
|
|
28375
|
+
colorGrid.appendChild(highlightControls);
|
|
28376
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28377
|
+
colorSection.appendChild(colorGrid);
|
|
26468
28378
|
container.appendChild(colorSection);
|
|
26469
28379
|
return container;
|
|
26470
28380
|
}
|
|
28381
|
+
rebuildFontSelect() {
|
|
28382
|
+
if (!this.fontFamilySelect)
|
|
28383
|
+
return;
|
|
28384
|
+
const currentValue = this.fontFamilySelect.value;
|
|
28385
|
+
this.fontFamilySelect.innerHTML = '';
|
|
28386
|
+
for (const family of this.fontFamilies) {
|
|
28387
|
+
const option = document.createElement('option');
|
|
28388
|
+
option.value = family;
|
|
28389
|
+
option.textContent = family;
|
|
28390
|
+
option.style.fontFamily = family;
|
|
28391
|
+
this.fontFamilySelect.appendChild(option);
|
|
28392
|
+
}
|
|
28393
|
+
// Restore selection if the font still exists
|
|
28394
|
+
if (this.fontFamilies.includes(currentValue)) {
|
|
28395
|
+
this.fontFamilySelect.value = currentValue;
|
|
28396
|
+
}
|
|
28397
|
+
}
|
|
26471
28398
|
updateFromEditor() {
|
|
26472
28399
|
if (!this.editor)
|
|
26473
28400
|
return;
|
|
@@ -26521,9 +28448,15 @@ class FormattingPane extends BasePane {
|
|
|
26521
28448
|
this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
|
|
26522
28449
|
this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
|
|
26523
28450
|
}
|
|
28451
|
+
else {
|
|
28452
|
+
this.bulletListBtn?.classList.remove('pc-pane-button--active');
|
|
28453
|
+
this.numberedListBtn?.classList.remove('pc-pane-button--active');
|
|
28454
|
+
}
|
|
26524
28455
|
}
|
|
26525
28456
|
catch {
|
|
26526
28457
|
// No text editing active
|
|
28458
|
+
this.bulletListBtn?.classList.remove('pc-pane-button--active');
|
|
28459
|
+
this.numberedListBtn?.classList.remove('pc-pane-button--active');
|
|
26527
28460
|
}
|
|
26528
28461
|
}
|
|
26529
28462
|
getSelection() {
|
|
@@ -26685,10 +28618,10 @@ class HyperlinkPane extends BasePane {
|
|
|
26685
28618
|
const container = document.createElement('div');
|
|
26686
28619
|
// URL input
|
|
26687
28620
|
this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
|
|
26688
|
-
container.appendChild(this.createFormGroup('URL', this.urlInput));
|
|
28621
|
+
container.appendChild(this.createFormGroup('URL:', this.urlInput));
|
|
26689
28622
|
// Title input
|
|
26690
28623
|
this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
|
|
26691
|
-
container.appendChild(this.createFormGroup('Title', this.titleInput));
|
|
28624
|
+
container.appendChild(this.createFormGroup('Title:', this.titleInput));
|
|
26692
28625
|
// Apply button
|
|
26693
28626
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
26694
28627
|
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
@@ -26839,10 +28772,10 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26839
28772
|
const container = document.createElement('div');
|
|
26840
28773
|
// Field name input
|
|
26841
28774
|
this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
|
|
26842
|
-
container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
|
|
28775
|
+
container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
|
|
26843
28776
|
// Default value input
|
|
26844
28777
|
this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
|
|
26845
|
-
container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
|
|
28778
|
+
container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
|
|
26846
28779
|
// Value type select
|
|
26847
28780
|
this.valueTypeSelect = this.createSelect([
|
|
26848
28781
|
{ value: '', label: '(None)' },
|
|
@@ -26851,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26851
28784
|
{ value: 'date', label: 'Date' }
|
|
26852
28785
|
]);
|
|
26853
28786
|
this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
|
|
26854
|
-
container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
|
|
28787
|
+
container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
|
|
26855
28788
|
// Number format group
|
|
26856
28789
|
this.numberFormatGroup = this.createSection();
|
|
26857
28790
|
this.numberFormatGroup.style.display = 'none';
|
|
@@ -26861,7 +28794,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26861
28794
|
{ value: '0,0', label: 'Thousands separator (0,0)' },
|
|
26862
28795
|
{ value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
|
|
26863
28796
|
]);
|
|
26864
|
-
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
|
|
28797
|
+
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
|
|
26865
28798
|
container.appendChild(this.numberFormatGroup);
|
|
26866
28799
|
// Currency format group
|
|
26867
28800
|
this.currencyFormatGroup = this.createSection();
|
|
@@ -26872,7 +28805,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26872
28805
|
{ value: 'GBP', label: 'GBP' },
|
|
26873
28806
|
{ value: 'JPY', label: 'JPY' }
|
|
26874
28807
|
]);
|
|
26875
|
-
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
|
|
28808
|
+
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
|
|
26876
28809
|
container.appendChild(this.currencyFormatGroup);
|
|
26877
28810
|
// Date format group
|
|
26878
28811
|
this.dateFormatGroup = this.createSection();
|
|
@@ -26883,7 +28816,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26883
28816
|
{ value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
|
|
26884
28817
|
{ value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
|
|
26885
28818
|
]);
|
|
26886
|
-
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
|
|
28819
|
+
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
|
|
26887
28820
|
container.appendChild(this.dateFormatGroup);
|
|
26888
28821
|
// Apply button
|
|
26889
28822
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
@@ -27045,12 +28978,17 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27045
28978
|
if (this.editor) {
|
|
27046
28979
|
// Listen for repeating section selection
|
|
27047
28980
|
const selectionHandler = (event) => {
|
|
27048
|
-
|
|
27049
|
-
|
|
28981
|
+
const sel = event.selection || event;
|
|
28982
|
+
if (sel.type === 'repeating-section' && sel.sectionId) {
|
|
28983
|
+
const section = this.editor?.getRepeatingSection(sel.sectionId);
|
|
27050
28984
|
if (section) {
|
|
27051
28985
|
this.showSection(section);
|
|
27052
28986
|
}
|
|
27053
28987
|
}
|
|
28988
|
+
else {
|
|
28989
|
+
// Selection changed away from repeating section — hide pane
|
|
28990
|
+
this.hideSection();
|
|
28991
|
+
}
|
|
27054
28992
|
};
|
|
27055
28993
|
const removedHandler = () => {
|
|
27056
28994
|
this.hideSection();
|
|
@@ -27067,7 +29005,7 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27067
29005
|
const container = document.createElement('div');
|
|
27068
29006
|
// Field path input
|
|
27069
29007
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27070
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29008
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27071
29009
|
hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
|
|
27072
29010
|
}));
|
|
27073
29011
|
// Apply button
|
|
@@ -27167,6 +29105,158 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27167
29105
|
}
|
|
27168
29106
|
}
|
|
27169
29107
|
|
|
29108
|
+
/**
|
|
29109
|
+
* ConditionalSectionPane - Edit conditional section properties.
|
|
29110
|
+
*
|
|
29111
|
+
* Shows:
|
|
29112
|
+
* - Predicate input (boolean expression in merge data)
|
|
29113
|
+
* - Position information
|
|
29114
|
+
*
|
|
29115
|
+
* Uses the PCEditor public API:
|
|
29116
|
+
* - editor.getConditionalSection()
|
|
29117
|
+
* - editor.updateConditionalSectionPredicate()
|
|
29118
|
+
* - editor.removeConditionalSection()
|
|
29119
|
+
*/
|
|
29120
|
+
class ConditionalSectionPane extends BasePane {
|
|
29121
|
+
constructor(id = 'conditional-section', options = {}) {
|
|
29122
|
+
super(id, { className: 'pc-pane-conditional-section', ...options });
|
|
29123
|
+
this.predicateInput = null;
|
|
29124
|
+
this.positionHint = null;
|
|
29125
|
+
this.currentSection = null;
|
|
29126
|
+
this.onApplyCallback = options.onApply;
|
|
29127
|
+
this.onRemoveCallback = options.onRemove;
|
|
29128
|
+
}
|
|
29129
|
+
attach(options) {
|
|
29130
|
+
super.attach(options);
|
|
29131
|
+
if (this.editor) {
|
|
29132
|
+
// Listen for conditional section selection
|
|
29133
|
+
const selectionHandler = (event) => {
|
|
29134
|
+
const sel = event.selection || event;
|
|
29135
|
+
if (sel.type === 'conditional-section' && sel.sectionId) {
|
|
29136
|
+
const section = this.editor?.getConditionalSection(sel.sectionId);
|
|
29137
|
+
if (section) {
|
|
29138
|
+
this.showSection(section);
|
|
29139
|
+
}
|
|
29140
|
+
}
|
|
29141
|
+
else {
|
|
29142
|
+
// Selection changed away from conditional section — hide pane
|
|
29143
|
+
this.hideSection();
|
|
29144
|
+
}
|
|
29145
|
+
};
|
|
29146
|
+
const removedHandler = () => {
|
|
29147
|
+
this.hideSection();
|
|
29148
|
+
};
|
|
29149
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29150
|
+
this.editor.on('conditional-section-removed', removedHandler);
|
|
29151
|
+
this.eventCleanup.push(() => {
|
|
29152
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29153
|
+
this.editor?.off('conditional-section-removed', removedHandler);
|
|
29154
|
+
});
|
|
29155
|
+
}
|
|
29156
|
+
}
|
|
29157
|
+
createContent() {
|
|
29158
|
+
const container = document.createElement('div');
|
|
29159
|
+
// Predicate input
|
|
29160
|
+
this.predicateInput = this.createTextInput({ placeholder: 'isActive' });
|
|
29161
|
+
container.appendChild(this.createFormGroup('Condition:', this.predicateInput, {
|
|
29162
|
+
hint: 'Boolean expression evaluated against merge data (e.g., "isActive", "count > 0")'
|
|
29163
|
+
}));
|
|
29164
|
+
// Apply button
|
|
29165
|
+
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
29166
|
+
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
29167
|
+
container.appendChild(applyBtn);
|
|
29168
|
+
// Remove button
|
|
29169
|
+
const removeBtn = this.createButton('Remove Condition', { variant: 'danger' });
|
|
29170
|
+
removeBtn.style.marginTop = '0.5rem';
|
|
29171
|
+
this.addButtonListener(removeBtn, () => this.removeSection());
|
|
29172
|
+
container.appendChild(removeBtn);
|
|
29173
|
+
// Position hint
|
|
29174
|
+
this.positionHint = this.createHint('');
|
|
29175
|
+
container.appendChild(this.positionHint);
|
|
29176
|
+
return container;
|
|
29177
|
+
}
|
|
29178
|
+
/**
|
|
29179
|
+
* Show the pane with the given section.
|
|
29180
|
+
*/
|
|
29181
|
+
showSection(section) {
|
|
29182
|
+
this.currentSection = section;
|
|
29183
|
+
if (this.predicateInput) {
|
|
29184
|
+
this.predicateInput.value = section.predicate;
|
|
29185
|
+
}
|
|
29186
|
+
if (this.positionHint) {
|
|
29187
|
+
this.positionHint.textContent = `Condition from position ${section.startIndex} to ${section.endIndex}`;
|
|
29188
|
+
}
|
|
29189
|
+
this.show();
|
|
29190
|
+
}
|
|
29191
|
+
/**
|
|
29192
|
+
* Hide the pane and clear the current section.
|
|
29193
|
+
*/
|
|
29194
|
+
hideSection() {
|
|
29195
|
+
this.currentSection = null;
|
|
29196
|
+
this.hide();
|
|
29197
|
+
}
|
|
29198
|
+
applyChanges() {
|
|
29199
|
+
if (!this.editor || !this.currentSection) {
|
|
29200
|
+
this.onApplyCallback?.(false, new Error('No section selected'));
|
|
29201
|
+
return;
|
|
29202
|
+
}
|
|
29203
|
+
const predicate = this.predicateInput?.value.trim();
|
|
29204
|
+
if (!predicate) {
|
|
29205
|
+
this.onApplyCallback?.(false, new Error('Predicate cannot be empty'));
|
|
29206
|
+
return;
|
|
29207
|
+
}
|
|
29208
|
+
if (predicate === this.currentSection.predicate) {
|
|
29209
|
+
return; // No changes
|
|
29210
|
+
}
|
|
29211
|
+
try {
|
|
29212
|
+
const success = this.editor.updateConditionalSectionPredicate(this.currentSection.id, predicate);
|
|
29213
|
+
if (success) {
|
|
29214
|
+
this.currentSection = this.editor.getConditionalSection(this.currentSection.id) || null;
|
|
29215
|
+
if (this.currentSection) {
|
|
29216
|
+
this.showSection(this.currentSection);
|
|
29217
|
+
}
|
|
29218
|
+
this.onApplyCallback?.(true);
|
|
29219
|
+
}
|
|
29220
|
+
else {
|
|
29221
|
+
this.onApplyCallback?.(false, new Error('Failed to update section'));
|
|
29222
|
+
}
|
|
29223
|
+
}
|
|
29224
|
+
catch (error) {
|
|
29225
|
+
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
29226
|
+
}
|
|
29227
|
+
}
|
|
29228
|
+
removeSection() {
|
|
29229
|
+
if (!this.editor || !this.currentSection)
|
|
29230
|
+
return;
|
|
29231
|
+
try {
|
|
29232
|
+
this.editor.removeConditionalSection(this.currentSection.id);
|
|
29233
|
+
this.hideSection();
|
|
29234
|
+
this.onRemoveCallback?.(true);
|
|
29235
|
+
}
|
|
29236
|
+
catch {
|
|
29237
|
+
this.onRemoveCallback?.(false);
|
|
29238
|
+
}
|
|
29239
|
+
}
|
|
29240
|
+
/**
|
|
29241
|
+
* Get the currently selected section.
|
|
29242
|
+
*/
|
|
29243
|
+
getCurrentSection() {
|
|
29244
|
+
return this.currentSection;
|
|
29245
|
+
}
|
|
29246
|
+
/**
|
|
29247
|
+
* Check if a section is currently selected.
|
|
29248
|
+
*/
|
|
29249
|
+
hasSection() {
|
|
29250
|
+
return this.currentSection !== null;
|
|
29251
|
+
}
|
|
29252
|
+
/**
|
|
29253
|
+
* Update the pane from current editor state.
|
|
29254
|
+
*/
|
|
29255
|
+
update() {
|
|
29256
|
+
// Section pane doesn't auto-update - it's driven by selection events
|
|
29257
|
+
}
|
|
29258
|
+
}
|
|
29259
|
+
|
|
27170
29260
|
/**
|
|
27171
29261
|
* TableRowLoopPane - Edit table row loop properties.
|
|
27172
29262
|
*
|
|
@@ -27191,14 +29281,28 @@ class TableRowLoopPane extends BasePane {
|
|
|
27191
29281
|
}
|
|
27192
29282
|
attach(options) {
|
|
27193
29283
|
super.attach(options);
|
|
27194
|
-
|
|
27195
|
-
|
|
29284
|
+
if (this.editor) {
|
|
29285
|
+
// Auto-show when a table row loop is clicked
|
|
29286
|
+
const loopClickHandler = (data) => {
|
|
29287
|
+
this.showLoop(data.table, data.loop);
|
|
29288
|
+
};
|
|
29289
|
+
// Hide when selection changes away from a loop
|
|
29290
|
+
const selectionHandler = () => {
|
|
29291
|
+
this.hideLoop();
|
|
29292
|
+
};
|
|
29293
|
+
this.editor.on('table-row-loop-clicked', loopClickHandler);
|
|
29294
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29295
|
+
this.eventCleanup.push(() => {
|
|
29296
|
+
this.editor?.off('table-row-loop-clicked', loopClickHandler);
|
|
29297
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29298
|
+
});
|
|
29299
|
+
}
|
|
27196
29300
|
}
|
|
27197
29301
|
createContent() {
|
|
27198
29302
|
const container = document.createElement('div');
|
|
27199
29303
|
// Field path input
|
|
27200
29304
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27201
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29305
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27202
29306
|
hint: 'Path to array in merge data (e.g., "items" or "orders")'
|
|
27203
29307
|
}));
|
|
27204
29308
|
// Apply button
|
|
@@ -27358,56 +29462,63 @@ class TextBoxPane extends BasePane {
|
|
|
27358
29462
|
}
|
|
27359
29463
|
createContent() {
|
|
27360
29464
|
const container = document.createElement('div');
|
|
27361
|
-
// Position section
|
|
29465
|
+
// Position section - Type on same row as label
|
|
27362
29466
|
const positionSection = this.createSection('Position');
|
|
27363
29467
|
this.positionSelect = this.createSelect([
|
|
27364
29468
|
{ value: 'inline', label: 'Inline' },
|
|
27365
29469
|
{ value: 'block', label: 'Block' },
|
|
27366
29470
|
{ value: 'relative', label: 'Relative' }
|
|
27367
29471
|
], 'inline');
|
|
27368
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27369
|
-
|
|
29472
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29473
|
+
this.updateOffsetVisibility();
|
|
29474
|
+
this.applyChanges();
|
|
29475
|
+
});
|
|
29476
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27370
29477
|
// Offset group (only visible for relative positioning)
|
|
27371
29478
|
this.offsetGroup = document.createElement('div');
|
|
27372
29479
|
this.offsetGroup.style.display = 'none';
|
|
27373
29480
|
const offsetRow = this.createRow();
|
|
27374
29481
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27375
29482
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27376
|
-
|
|
27377
|
-
|
|
29483
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29484
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29485
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29486
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27378
29487
|
this.offsetGroup.appendChild(offsetRow);
|
|
27379
29488
|
positionSection.appendChild(this.offsetGroup);
|
|
27380
29489
|
container.appendChild(positionSection);
|
|
27381
|
-
// Background
|
|
27382
|
-
const bgSection = this.createSection(
|
|
29490
|
+
// Background - color on same row as label
|
|
29491
|
+
const bgSection = this.createSection();
|
|
27383
29492
|
this.bgColorInput = this.createColorInput('#ffffff');
|
|
27384
|
-
|
|
29493
|
+
this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
|
|
29494
|
+
bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
|
|
27385
29495
|
container.appendChild(bgSection);
|
|
27386
29496
|
// Border section
|
|
27387
29497
|
const borderSection = this.createSection('Border');
|
|
27388
29498
|
const borderRow = this.createRow();
|
|
27389
29499
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
|
|
27390
29500
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27391
|
-
|
|
27392
|
-
|
|
29501
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyChanges());
|
|
29502
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyChanges());
|
|
29503
|
+
borderRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
29504
|
+
borderRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
27393
29505
|
borderSection.appendChild(borderRow);
|
|
29506
|
+
// Border style on same row as label
|
|
27394
29507
|
this.borderStyleSelect = this.createSelect([
|
|
27395
29508
|
{ value: 'solid', label: 'Solid' },
|
|
27396
29509
|
{ value: 'dashed', label: 'Dashed' },
|
|
27397
29510
|
{ value: 'dotted', label: 'Dotted' },
|
|
27398
29511
|
{ value: 'none', label: 'None' }
|
|
27399
29512
|
], 'solid');
|
|
27400
|
-
|
|
29513
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
|
|
29514
|
+
borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27401
29515
|
container.appendChild(borderSection);
|
|
27402
|
-
// Padding
|
|
27403
|
-
const paddingSection = this.createSection(
|
|
29516
|
+
// Padding on same row as label
|
|
29517
|
+
const paddingSection = this.createSection();
|
|
27404
29518
|
this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
|
|
27405
|
-
|
|
29519
|
+
this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
|
|
29520
|
+
paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
|
|
27406
29521
|
container.appendChild(paddingSection);
|
|
27407
|
-
// Apply button
|
|
27408
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27409
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27410
|
-
container.appendChild(applyBtn);
|
|
27411
29522
|
return container;
|
|
27412
29523
|
}
|
|
27413
29524
|
updateFromSelection() {
|
|
@@ -27483,7 +29594,6 @@ class TextBoxPane extends BasePane {
|
|
|
27483
29594
|
}
|
|
27484
29595
|
applyChanges() {
|
|
27485
29596
|
if (!this.editor || !this.currentTextBox) {
|
|
27486
|
-
this.onApplyCallback?.(false, new Error('No text box selected'));
|
|
27487
29597
|
return;
|
|
27488
29598
|
}
|
|
27489
29599
|
const updates = {};
|
|
@@ -27519,12 +29629,7 @@ class TextBoxPane extends BasePane {
|
|
|
27519
29629
|
}
|
|
27520
29630
|
try {
|
|
27521
29631
|
const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
|
|
27522
|
-
|
|
27523
|
-
this.onApplyCallback?.(true);
|
|
27524
|
-
}
|
|
27525
|
-
else {
|
|
27526
|
-
this.onApplyCallback?.(false, new Error('Failed to update text box'));
|
|
27527
|
-
}
|
|
29632
|
+
this.onApplyCallback?.(success);
|
|
27528
29633
|
}
|
|
27529
29634
|
catch (error) {
|
|
27530
29635
|
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -27600,28 +29705,35 @@ class ImagePane extends BasePane {
|
|
|
27600
29705
|
}
|
|
27601
29706
|
createContent() {
|
|
27602
29707
|
const container = document.createElement('div');
|
|
27603
|
-
// Position section
|
|
29708
|
+
// Position section — with heading, matching TextBoxPane
|
|
27604
29709
|
const positionSection = this.createSection('Position');
|
|
27605
29710
|
this.positionSelect = this.createSelect([
|
|
27606
29711
|
{ value: 'inline', label: 'Inline' },
|
|
27607
29712
|
{ value: 'block', label: 'Block' },
|
|
27608
29713
|
{ value: 'relative', label: 'Relative' }
|
|
27609
29714
|
], 'inline');
|
|
27610
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27611
|
-
|
|
29715
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29716
|
+
this.updateOffsetVisibility();
|
|
29717
|
+
this.applyChanges();
|
|
29718
|
+
});
|
|
29719
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27612
29720
|
// Offset group (only visible for relative positioning)
|
|
27613
29721
|
this.offsetGroup = document.createElement('div');
|
|
27614
29722
|
this.offsetGroup.style.display = 'none';
|
|
27615
29723
|
const offsetRow = this.createRow();
|
|
27616
29724
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27617
29725
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27618
|
-
|
|
27619
|
-
|
|
29726
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29727
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29728
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29729
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27620
29730
|
this.offsetGroup.appendChild(offsetRow);
|
|
27621
29731
|
positionSection.appendChild(this.offsetGroup);
|
|
27622
29732
|
container.appendChild(positionSection);
|
|
27623
|
-
|
|
27624
|
-
|
|
29733
|
+
container.appendChild(document.createElement('hr'));
|
|
29734
|
+
// Display section — Fit Mode and Resize Mode with aligned labels
|
|
29735
|
+
const displaySection = document.createElement('div');
|
|
29736
|
+
displaySection.className = 'pc-pane-image-display';
|
|
27625
29737
|
this.fitModeSelect = this.createSelect([
|
|
27626
29738
|
{ value: 'contain', label: 'Contain' },
|
|
27627
29739
|
{ value: 'cover', label: 'Cover' },
|
|
@@ -27629,34 +29741,31 @@ class ImagePane extends BasePane {
|
|
|
27629
29741
|
{ value: 'none', label: 'None (original size)' },
|
|
27630
29742
|
{ value: 'tile', label: 'Tile' }
|
|
27631
29743
|
], 'contain');
|
|
27632
|
-
|
|
29744
|
+
this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
|
|
29745
|
+
displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
|
|
27633
29746
|
this.resizeModeSelect = this.createSelect([
|
|
27634
29747
|
{ value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
|
|
27635
29748
|
{ value: 'free', label: 'Free Resize' }
|
|
27636
29749
|
], 'locked-aspect-ratio');
|
|
27637
|
-
|
|
27638
|
-
|
|
27639
|
-
|
|
27640
|
-
|
|
29750
|
+
this.addImmediateApplyListener(this.resizeModeSelect, () => this.applyChanges());
|
|
29751
|
+
displaySection.appendChild(this.createFormGroup('Resize Mode:', this.resizeModeSelect, { inline: true }));
|
|
29752
|
+
container.appendChild(displaySection);
|
|
29753
|
+
container.appendChild(document.createElement('hr'));
|
|
29754
|
+
// Alt Text — inline row
|
|
27641
29755
|
this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
|
|
27642
|
-
|
|
27643
|
-
container.appendChild(
|
|
27644
|
-
|
|
27645
|
-
|
|
29756
|
+
this.addImmediateApplyListener(this.altTextInput, () => this.applyChanges());
|
|
29757
|
+
container.appendChild(this.createFormGroup('Alt Text:', this.altTextInput, { inline: true }));
|
|
29758
|
+
container.appendChild(document.createElement('hr'));
|
|
29759
|
+
// Source — change image button
|
|
27646
29760
|
this.fileInput = document.createElement('input');
|
|
27647
29761
|
this.fileInput.type = 'file';
|
|
27648
29762
|
this.fileInput.accept = 'image/*';
|
|
27649
29763
|
this.fileInput.style.display = 'none';
|
|
27650
29764
|
this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
|
|
27651
|
-
|
|
29765
|
+
container.appendChild(this.fileInput);
|
|
27652
29766
|
const changeSourceBtn = this.createButton('Change Image...');
|
|
27653
29767
|
this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
|
|
27654
|
-
|
|
27655
|
-
container.appendChild(sourceSection);
|
|
27656
|
-
// Apply button
|
|
27657
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27658
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27659
|
-
container.appendChild(applyBtn);
|
|
29768
|
+
container.appendChild(changeSourceBtn);
|
|
27660
29769
|
return container;
|
|
27661
29770
|
}
|
|
27662
29771
|
updateFromSelection() {
|
|
@@ -27838,8 +29947,9 @@ class TablePane extends BasePane {
|
|
|
27838
29947
|
// Default controls
|
|
27839
29948
|
this.defaultPaddingInput = null;
|
|
27840
29949
|
this.defaultBorderColorInput = null;
|
|
27841
|
-
//
|
|
27842
|
-
this.
|
|
29950
|
+
// Merge/split buttons
|
|
29951
|
+
this.mergeCellsBtn = null;
|
|
29952
|
+
this.splitCellBtn = null;
|
|
27843
29953
|
// Cell formatting controls
|
|
27844
29954
|
this.cellBgColorInput = null;
|
|
27845
29955
|
this.borderTopCheck = null;
|
|
@@ -27859,12 +29969,12 @@ class TablePane extends BasePane {
|
|
|
27859
29969
|
// Listen for selection/focus changes
|
|
27860
29970
|
const updateHandler = () => this.updateFromFocusedTable();
|
|
27861
29971
|
this.editor.on('selection-change', updateHandler);
|
|
27862
|
-
this.editor.on('
|
|
27863
|
-
this.editor.on('table-cell-selection', updateHandler);
|
|
29972
|
+
this.editor.on('tablecell-cursor-changed', updateHandler);
|
|
29973
|
+
this.editor.on('table-cell-selection-changed', updateHandler);
|
|
27864
29974
|
this.eventCleanup.push(() => {
|
|
27865
29975
|
this.editor?.off('selection-change', updateHandler);
|
|
27866
|
-
this.editor?.off('
|
|
27867
|
-
this.editor?.off('table-cell-selection', updateHandler);
|
|
29976
|
+
this.editor?.off('tablecell-cursor-changed', updateHandler);
|
|
29977
|
+
this.editor?.off('table-cell-selection-changed', updateHandler);
|
|
27868
29978
|
});
|
|
27869
29979
|
// Initial update
|
|
27870
29980
|
this.updateFromFocusedTable();
|
|
@@ -27874,78 +29984,78 @@ class TablePane extends BasePane {
|
|
|
27874
29984
|
const container = document.createElement('div');
|
|
27875
29985
|
// Structure section
|
|
27876
29986
|
const structureSection = this.createSection('Structure');
|
|
29987
|
+
// Rows/Columns info with aligned labels
|
|
27877
29988
|
const structureInfo = document.createElement('div');
|
|
27878
|
-
structureInfo.className = 'pc-pane-info
|
|
29989
|
+
structureInfo.className = 'pc-pane-table-structure-info';
|
|
27879
29990
|
this.rowCountDisplay = document.createElement('span');
|
|
29991
|
+
this.rowCountDisplay.className = 'pc-pane-info-value';
|
|
27880
29992
|
this.colCountDisplay = document.createElement('span');
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
rowInfo.appendChild(this.rowCountDisplay);
|
|
27885
|
-
const colInfo = document.createElement('div');
|
|
27886
|
-
colInfo.className = 'pc-pane-info';
|
|
27887
|
-
colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
|
|
27888
|
-
colInfo.appendChild(this.colCountDisplay);
|
|
27889
|
-
structureInfo.appendChild(rowInfo);
|
|
27890
|
-
structureInfo.appendChild(colInfo);
|
|
29993
|
+
this.colCountDisplay.className = 'pc-pane-info-value';
|
|
29994
|
+
structureInfo.appendChild(this.createFormGroup('Rows:', this.rowCountDisplay, { inline: true }));
|
|
29995
|
+
structureInfo.appendChild(this.createFormGroup('Columns:', this.colCountDisplay, { inline: true }));
|
|
27891
29996
|
structureSection.appendChild(structureInfo);
|
|
27892
|
-
// Row
|
|
27893
|
-
const
|
|
29997
|
+
// Row buttons
|
|
29998
|
+
const rowBtns = this.createButtonGroup();
|
|
27894
29999
|
const addRowBtn = this.createButton('+ Row');
|
|
27895
30000
|
this.addButtonListener(addRowBtn, () => this.insertRow());
|
|
27896
30001
|
const removeRowBtn = this.createButton('- Row');
|
|
27897
30002
|
this.addButtonListener(removeRowBtn, () => this.removeRow());
|
|
30003
|
+
rowBtns.appendChild(addRowBtn);
|
|
30004
|
+
rowBtns.appendChild(removeRowBtn);
|
|
30005
|
+
structureSection.appendChild(rowBtns);
|
|
30006
|
+
// Column buttons (separate row)
|
|
30007
|
+
const colBtns = this.createButtonGroup();
|
|
27898
30008
|
const addColBtn = this.createButton('+ Column');
|
|
27899
30009
|
this.addButtonListener(addColBtn, () => this.insertColumn());
|
|
27900
30010
|
const removeColBtn = this.createButton('- Column');
|
|
27901
30011
|
this.addButtonListener(removeColBtn, () => this.removeColumn());
|
|
27902
|
-
|
|
27903
|
-
|
|
27904
|
-
|
|
27905
|
-
|
|
27906
|
-
structureSection.appendChild(
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
const headersSection = this.createSection('Headers');
|
|
27910
|
-
const headerRow = this.createRow();
|
|
30012
|
+
colBtns.appendChild(addColBtn);
|
|
30013
|
+
colBtns.appendChild(removeColBtn);
|
|
30014
|
+
structureSection.appendChild(colBtns);
|
|
30015
|
+
// Header rows/cols (with separator and aligned labels)
|
|
30016
|
+
structureSection.appendChild(document.createElement('hr'));
|
|
30017
|
+
const headersGroup = document.createElement('div');
|
|
30018
|
+
headersGroup.className = 'pc-pane-table-headers';
|
|
27911
30019
|
this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
30020
|
+
this.addImmediateApplyListener(this.headerRowInput, () => this.applyHeaders());
|
|
30021
|
+
headersGroup.appendChild(this.createFormGroup('Header Rows:', this.headerRowInput, { inline: true }));
|
|
27912
30022
|
this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
headersSection.appendChild(applyHeadersBtn);
|
|
27919
|
-
container.appendChild(headersSection);
|
|
27920
|
-
// Row Loop section
|
|
27921
|
-
const loopSection = this.createSection('Row Loop');
|
|
27922
|
-
this.loopFieldInput = this.createTextInput({ placeholder: 'items' });
|
|
27923
|
-
loopSection.appendChild(this.createFormGroup('Array Field', this.loopFieldInput, {
|
|
27924
|
-
hint: 'Creates a loop on the currently focused row'
|
|
27925
|
-
}));
|
|
27926
|
-
const createLoopBtn = this.createButton('Create Row Loop');
|
|
27927
|
-
this.addButtonListener(createLoopBtn, () => this.createRowLoop());
|
|
27928
|
-
loopSection.appendChild(createLoopBtn);
|
|
27929
|
-
container.appendChild(loopSection);
|
|
27930
|
-
// Defaults section
|
|
30023
|
+
this.addImmediateApplyListener(this.headerColInput, () => this.applyHeaders());
|
|
30024
|
+
headersGroup.appendChild(this.createFormGroup('Header Cols:', this.headerColInput, { inline: true }));
|
|
30025
|
+
structureSection.appendChild(headersGroup);
|
|
30026
|
+
container.appendChild(structureSection);
|
|
30027
|
+
// Defaults section (aligned labels)
|
|
27931
30028
|
const defaultsSection = this.createSection('Defaults');
|
|
27932
|
-
const
|
|
30029
|
+
const defaultsGroup = document.createElement('div');
|
|
30030
|
+
defaultsGroup.className = 'pc-pane-table-defaults';
|
|
27933
30031
|
this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
|
|
30032
|
+
this.addImmediateApplyListener(this.defaultPaddingInput, () => this.applyDefaults());
|
|
30033
|
+
defaultsGroup.appendChild(this.createFormGroup('Padding:', this.defaultPaddingInput, { inline: true }));
|
|
27934
30034
|
this.defaultBorderColorInput = this.createColorInput('#cccccc');
|
|
27935
|
-
|
|
27936
|
-
|
|
27937
|
-
defaultsSection.appendChild(
|
|
27938
|
-
const applyDefaultsBtn = this.createButton('Apply Defaults');
|
|
27939
|
-
this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
|
|
27940
|
-
defaultsSection.appendChild(applyDefaultsBtn);
|
|
30035
|
+
this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
|
|
30036
|
+
defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
|
|
30037
|
+
defaultsSection.appendChild(defaultsGroup);
|
|
27941
30038
|
container.appendChild(defaultsSection);
|
|
27942
30039
|
// Cell formatting section
|
|
27943
30040
|
const cellSection = this.createSection('Cell Formatting');
|
|
27944
30041
|
this.cellSelectionDisplay = this.createHint('No cell selected');
|
|
27945
30042
|
cellSection.appendChild(this.cellSelectionDisplay);
|
|
27946
|
-
//
|
|
30043
|
+
// Merge/Split buttons
|
|
30044
|
+
const mergeBtnGroup = this.createButtonGroup();
|
|
30045
|
+
this.mergeCellsBtn = this.createButton('Merge Cells');
|
|
30046
|
+
this.mergeCellsBtn.disabled = true;
|
|
30047
|
+
this.splitCellBtn = this.createButton('Split Cell');
|
|
30048
|
+
this.splitCellBtn.disabled = true;
|
|
30049
|
+
this.addButtonListener(this.mergeCellsBtn, () => this.doMergeCells());
|
|
30050
|
+
this.addButtonListener(this.splitCellBtn, () => this.doSplitCell());
|
|
30051
|
+
mergeBtnGroup.appendChild(this.mergeCellsBtn);
|
|
30052
|
+
mergeBtnGroup.appendChild(this.splitCellBtn);
|
|
30053
|
+
cellSection.appendChild(mergeBtnGroup);
|
|
30054
|
+
cellSection.appendChild(document.createElement('hr'));
|
|
30055
|
+
// Background — inline
|
|
27947
30056
|
this.cellBgColorInput = this.createColorInput('#ffffff');
|
|
27948
|
-
|
|
30057
|
+
this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
|
|
30058
|
+
cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
|
|
27949
30059
|
// Border checkboxes
|
|
27950
30060
|
const borderChecks = document.createElement('div');
|
|
27951
30061
|
borderChecks.className = 'pc-pane-row';
|
|
@@ -27977,24 +30087,29 @@ class TablePane extends BasePane {
|
|
|
27977
30087
|
checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
|
|
27978
30088
|
if (checkLabels[3])
|
|
27979
30089
|
checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
|
|
27980
|
-
|
|
30090
|
+
// Add change listeners for immediate apply on checkboxes
|
|
30091
|
+
for (const check of [this.borderTopCheck, this.borderRightCheck, this.borderBottomCheck, this.borderLeftCheck]) {
|
|
30092
|
+
check.addEventListener('change', () => this.applyCellFormatting());
|
|
30093
|
+
}
|
|
30094
|
+
cellSection.appendChild(this.createFormGroup('Borders:', borderChecks));
|
|
27981
30095
|
// Border properties
|
|
27982
30096
|
const borderPropsRow = this.createRow();
|
|
27983
30097
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
|
|
27984
30098
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27985
|
-
|
|
27986
|
-
|
|
30099
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyCellFormatting());
|
|
30100
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyCellFormatting());
|
|
30101
|
+
borderPropsRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
30102
|
+
borderPropsRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
27987
30103
|
cellSection.appendChild(borderPropsRow);
|
|
30104
|
+
// Style — inline
|
|
27988
30105
|
this.borderStyleSelect = this.createSelect([
|
|
27989
30106
|
{ value: 'solid', label: 'Solid' },
|
|
27990
30107
|
{ value: 'dashed', label: 'Dashed' },
|
|
27991
30108
|
{ value: 'dotted', label: 'Dotted' },
|
|
27992
30109
|
{ value: 'none', label: 'None' }
|
|
27993
30110
|
], 'solid');
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
|
|
27997
|
-
cellSection.appendChild(applyCellBtn);
|
|
30111
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
|
|
30112
|
+
cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27998
30113
|
container.appendChild(cellSection);
|
|
27999
30114
|
return container;
|
|
28000
30115
|
}
|
|
@@ -28059,6 +30174,15 @@ class TablePane extends BasePane {
|
|
|
28059
30174
|
return;
|
|
28060
30175
|
const focusedCell = table.focusedCell;
|
|
28061
30176
|
const selectedRange = table.selectedRange;
|
|
30177
|
+
// Update merge/split button states
|
|
30178
|
+
if (this.mergeCellsBtn) {
|
|
30179
|
+
const canMerge = selectedRange ? table.canMergeRange(selectedRange).canMerge : false;
|
|
30180
|
+
this.mergeCellsBtn.disabled = !canMerge;
|
|
30181
|
+
}
|
|
30182
|
+
if (this.splitCellBtn) {
|
|
30183
|
+
const canSplit = focusedCell ? table.canSplitCell(focusedCell.row, focusedCell.col).canSplit : false;
|
|
30184
|
+
this.splitCellBtn.disabled = !canSplit;
|
|
30185
|
+
}
|
|
28062
30186
|
if (selectedRange) {
|
|
28063
30187
|
const count = (selectedRange.end.row - selectedRange.start.row + 1) *
|
|
28064
30188
|
(selectedRange.end.col - selectedRange.start.col + 1);
|
|
@@ -28216,20 +30340,20 @@ class TablePane extends BasePane {
|
|
|
28216
30340
|
hasTable() {
|
|
28217
30341
|
return this.currentTable !== null;
|
|
28218
30342
|
}
|
|
28219
|
-
|
|
28220
|
-
if (!this.editor || !this.currentTable)
|
|
28221
|
-
this.onApplyCallback?.(false, new Error('No table focused'));
|
|
30343
|
+
doMergeCells() {
|
|
30344
|
+
if (!this.editor || !this.currentTable)
|
|
28222
30345
|
return;
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
28226
|
-
|
|
30346
|
+
this.editor.tableMergeCells(this.currentTable);
|
|
30347
|
+
this.updateFromFocusedTable();
|
|
30348
|
+
}
|
|
30349
|
+
doSplitCell() {
|
|
30350
|
+
if (!this.editor || !this.currentTable)
|
|
28227
30351
|
return;
|
|
28228
|
-
|
|
28229
|
-
|
|
28230
|
-
|
|
28231
|
-
this.editor.
|
|
28232
|
-
this.
|
|
30352
|
+
const focused = this.currentTable.focusedCell;
|
|
30353
|
+
if (!focused)
|
|
30354
|
+
return;
|
|
30355
|
+
this.editor.tableSplitCell(this.currentTable, focused.row, focused.col);
|
|
30356
|
+
this.updateFromFocusedTable();
|
|
28233
30357
|
}
|
|
28234
30358
|
/**
|
|
28235
30359
|
* Update the pane from current editor state.
|
|
@@ -28245,6 +30369,8 @@ exports.BasePane = BasePane;
|
|
|
28245
30369
|
exports.BaseTextRegion = BaseTextRegion;
|
|
28246
30370
|
exports.BodyTextRegion = BodyTextRegion;
|
|
28247
30371
|
exports.ClipboardManager = ClipboardManager;
|
|
30372
|
+
exports.ConditionalSectionManager = ConditionalSectionManager;
|
|
30373
|
+
exports.ConditionalSectionPane = ConditionalSectionPane;
|
|
28248
30374
|
exports.ContentAnalyzer = ContentAnalyzer;
|
|
28249
30375
|
exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
|
|
28250
30376
|
exports.Document = Document;
|
|
@@ -28255,6 +30381,7 @@ exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
|
|
|
28255
30381
|
exports.EmbeddedObjectManager = EmbeddedObjectManager;
|
|
28256
30382
|
exports.EventEmitter = EventEmitter;
|
|
28257
30383
|
exports.FlowingTextContent = FlowingTextContent;
|
|
30384
|
+
exports.FontManager = FontManager;
|
|
28258
30385
|
exports.FooterTextRegion = FooterTextRegion;
|
|
28259
30386
|
exports.FormattingPane = FormattingPane;
|
|
28260
30387
|
exports.HeaderTextRegion = HeaderTextRegion;
|
|
@@ -28270,6 +30397,7 @@ exports.PDFImportError = PDFImportError;
|
|
|
28270
30397
|
exports.PDFImporter = PDFImporter;
|
|
28271
30398
|
exports.PDFParser = PDFParser;
|
|
28272
30399
|
exports.Page = Page;
|
|
30400
|
+
exports.PredicateEvaluator = PredicateEvaluator;
|
|
28273
30401
|
exports.RegionManager = RegionManager;
|
|
28274
30402
|
exports.RepeatingSectionManager = RepeatingSectionManager;
|
|
28275
30403
|
exports.RepeatingSectionPane = RepeatingSectionPane;
|