@productcloudos/editor 1.0.6 → 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 +2360 -310
- package/dist/pc-editor.esm.js.map +1 -1
- package/dist/pc-editor.js +2363 -309
- 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 +74 -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.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 +1 -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).
|
|
@@ -7851,6 +8259,10 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7851
8259
|
if (this._rowLoops.size > 0) {
|
|
7852
8260
|
this.renderRowLoopIndicators(ctx);
|
|
7853
8261
|
}
|
|
8262
|
+
// Render row conditional indicators
|
|
8263
|
+
if (this._rowConditionals.size > 0) {
|
|
8264
|
+
this.renderRowConditionalIndicators(ctx);
|
|
8265
|
+
}
|
|
7854
8266
|
// Render cell range selection highlight
|
|
7855
8267
|
if (this._selectedRange) {
|
|
7856
8268
|
this.renderRangeSelection(ctx);
|
|
@@ -7865,11 +8277,54 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7865
8277
|
}
|
|
7866
8278
|
}
|
|
7867
8279
|
/**
|
|
7868
|
-
*
|
|
8280
|
+
* Select a row loop by ID (for pane display).
|
|
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.
|
|
7869
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
|
+
}
|
|
7870
8324
|
renderRowLoopIndicators(ctx) {
|
|
7871
|
-
const
|
|
7872
|
-
const
|
|
8325
|
+
const color = TableObject.LOOP_COLOR;
|
|
8326
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8327
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
7873
8328
|
// Calculate row Y positions if not cached
|
|
7874
8329
|
let rowPositions = this._cachedRowPositions;
|
|
7875
8330
|
if (rowPositions.length === 0) {
|
|
@@ -7880,12 +8335,8 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7880
8335
|
y += row.calculatedHeight;
|
|
7881
8336
|
}
|
|
7882
8337
|
}
|
|
7883
|
-
// Colors for different loops (cycle through these)
|
|
7884
|
-
const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
|
|
7885
|
-
let colorIndex = 0;
|
|
7886
8338
|
for (const loop of this._rowLoops.values()) {
|
|
7887
|
-
const
|
|
7888
|
-
colorIndex++;
|
|
8339
|
+
const isSelected = this._selectedRowLoopId === loop.id;
|
|
7889
8340
|
// Calculate the Y range for this loop
|
|
7890
8341
|
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
7891
8342
|
let endY = startY;
|
|
@@ -7895,31 +8346,149 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7895
8346
|
const loopHeight = endY - startY;
|
|
7896
8347
|
// Draw colored stripe on left side
|
|
7897
8348
|
ctx.fillStyle = color;
|
|
7898
|
-
ctx.fillRect(-
|
|
7899
|
-
// 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
|
|
7900
8358
|
ctx.save();
|
|
7901
8359
|
ctx.font = '10px Arial';
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
const
|
|
7905
|
-
const
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
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);
|
|
7909
8385
|
ctx.restore();
|
|
7910
|
-
|
|
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
|
|
7911
8459
|
ctx.strokeStyle = color;
|
|
7912
8460
|
ctx.lineWidth = 1;
|
|
7913
8461
|
ctx.beginPath();
|
|
7914
|
-
|
|
7915
|
-
ctx.
|
|
7916
|
-
ctx.lineTo(-indicatorWidth - 6, startY);
|
|
7917
|
-
ctx.lineTo(-indicatorWidth - 6, startY + 6);
|
|
7918
|
-
// Bottom bracket
|
|
7919
|
-
ctx.moveTo(-indicatorWidth - 2, endY);
|
|
7920
|
-
ctx.lineTo(-indicatorWidth - 6, endY);
|
|
7921
|
-
ctx.lineTo(-indicatorWidth - 6, endY - 6);
|
|
8462
|
+
ctx.moveTo(totalWidth + 4, startY);
|
|
8463
|
+
ctx.lineTo(totalWidth + 4, endY);
|
|
7922
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();
|
|
7923
8492
|
}
|
|
7924
8493
|
}
|
|
7925
8494
|
/**
|
|
@@ -8038,6 +8607,14 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8038
8607
|
columns: this._columns.map(col => ({ ...col })),
|
|
8039
8608
|
rows: this._rows.map(row => row.toData()),
|
|
8040
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,
|
|
8041
8618
|
defaultCellPadding: this._defaultCellPadding,
|
|
8042
8619
|
defaultBorderColor: this._defaultBorderColor,
|
|
8043
8620
|
defaultBorderWidth: this._defaultBorderWidth,
|
|
@@ -8081,6 +8658,17 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8081
8658
|
});
|
|
8082
8659
|
}
|
|
8083
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
|
+
}
|
|
8084
8672
|
table.updateCoveredCells();
|
|
8085
8673
|
return table;
|
|
8086
8674
|
}
|
|
@@ -8110,6 +8698,18 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8110
8698
|
});
|
|
8111
8699
|
}
|
|
8112
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
|
+
}
|
|
8113
8713
|
// Restore defaults
|
|
8114
8714
|
if (data.data.defaultCellPadding !== undefined) {
|
|
8115
8715
|
this._defaultCellPadding = data.data.defaultCellPadding;
|
|
@@ -8129,6 +8729,13 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8129
8729
|
return TableObject.fromData(this.toData());
|
|
8130
8730
|
}
|
|
8131
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
|
|
8132
8739
|
|
|
8133
8740
|
/**
|
|
8134
8741
|
* TableResizeHandler - Handles column and row resize operations for tables.
|
|
@@ -8514,6 +9121,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8514
9121
|
this.substitutionFields = new SubstitutionFieldManager();
|
|
8515
9122
|
this.embeddedObjects = new EmbeddedObjectManager();
|
|
8516
9123
|
this.repeatingSections = new RepeatingSectionManager();
|
|
9124
|
+
this.conditionalSections = new ConditionalSectionManager();
|
|
8517
9125
|
this.hyperlinks = new HyperlinkManager();
|
|
8518
9126
|
this.layout = new TextLayout();
|
|
8519
9127
|
this.setupEventForwarding();
|
|
@@ -8551,6 +9159,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8551
9159
|
this.substitutionFields.handleDeletion(data.start, data.length);
|
|
8552
9160
|
this.embeddedObjects.handleDeletion(data.start, data.length);
|
|
8553
9161
|
this.repeatingSections.handleDeletion(data.start, data.length);
|
|
9162
|
+
this.conditionalSections.handleDeletion(data.start, data.length);
|
|
8554
9163
|
this.paragraphFormatting.handleDeletion(data.start, data.length);
|
|
8555
9164
|
this.hyperlinks.handleDeletion(data.start, data.length);
|
|
8556
9165
|
this.emit('content-changed', {
|
|
@@ -8602,6 +9211,16 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8602
9211
|
this.repeatingSections.on('section-updated', (data) => {
|
|
8603
9212
|
this.emit('repeating-section-updated', data);
|
|
8604
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
|
+
});
|
|
8605
9224
|
// Forward hyperlink events
|
|
8606
9225
|
this.hyperlinks.on('hyperlink-added', (data) => {
|
|
8607
9226
|
this.emit('hyperlink-added', data);
|
|
@@ -8659,6 +9278,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8659
9278
|
this.substitutionFields.shiftFields(insertAt, text.length);
|
|
8660
9279
|
this.embeddedObjects.shiftObjects(insertAt, text.length);
|
|
8661
9280
|
this.repeatingSections.shiftSections(insertAt, text.length);
|
|
9281
|
+
this.conditionalSections.shiftSections(insertAt, text.length);
|
|
8662
9282
|
this.hyperlinks.shiftHyperlinks(insertAt, text.length);
|
|
8663
9283
|
// Insert the text first so we have the full content
|
|
8664
9284
|
this.textState.insertText(text, insertAt);
|
|
@@ -8731,6 +9351,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8731
9351
|
this.substitutionFields.shiftFields(position, text.length);
|
|
8732
9352
|
this.embeddedObjects.shiftObjects(position, text.length);
|
|
8733
9353
|
this.repeatingSections.shiftSections(position, text.length);
|
|
9354
|
+
this.conditionalSections.shiftSections(position, text.length);
|
|
8734
9355
|
this.hyperlinks.shiftHyperlinks(position, text.length);
|
|
8735
9356
|
// Insert the text
|
|
8736
9357
|
const content = this.textState.getText();
|
|
@@ -8748,6 +9369,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8748
9369
|
this.substitutionFields.handleDeletion(position, length);
|
|
8749
9370
|
this.embeddedObjects.handleDeletion(position, length);
|
|
8750
9371
|
this.repeatingSections.handleDeletion(position, length);
|
|
9372
|
+
this.conditionalSections.handleDeletion(position, length);
|
|
8751
9373
|
this.paragraphFormatting.handleDeletion(position, length);
|
|
8752
9374
|
this.hyperlinks.handleDeletion(position, length);
|
|
8753
9375
|
// Delete the text
|
|
@@ -9137,6 +9759,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9137
9759
|
this.substitutionFields.shiftFields(insertAt, 1);
|
|
9138
9760
|
this.embeddedObjects.shiftObjects(insertAt, 1);
|
|
9139
9761
|
this.repeatingSections.shiftSections(insertAt, 1);
|
|
9762
|
+
this.conditionalSections.shiftSections(insertAt, 1);
|
|
9140
9763
|
// Insert the placeholder character
|
|
9141
9764
|
this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
|
|
9142
9765
|
// Shift paragraph formatting with the complete content
|
|
@@ -9384,6 +10007,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9384
10007
|
this.substitutionFields.clear();
|
|
9385
10008
|
this.embeddedObjects.clear();
|
|
9386
10009
|
this.repeatingSections.clear();
|
|
10010
|
+
this.conditionalSections.clear();
|
|
9387
10011
|
this.hyperlinks.clear();
|
|
9388
10012
|
}
|
|
9389
10013
|
// ============================================
|
|
@@ -9740,44 +10364,60 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9740
10364
|
// List Operations
|
|
9741
10365
|
// ============================================
|
|
9742
10366
|
/**
|
|
9743
|
-
*
|
|
10367
|
+
* Get paragraph starts affected by the current selection or cursor position.
|
|
9744
10368
|
*/
|
|
9745
|
-
|
|
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
|
+
}
|
|
9746
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() {
|
|
9747
10382
|
const content = this.textState.getText();
|
|
9748
|
-
const
|
|
9749
|
-
|
|
9750
|
-
|
|
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() });
|
|
9751
10388
|
}
|
|
9752
10389
|
/**
|
|
9753
|
-
* Toggle numbered list for the current paragraph
|
|
10390
|
+
* Toggle numbered list for the current paragraph(s) in selection.
|
|
9754
10391
|
*/
|
|
9755
10392
|
toggleNumberedList() {
|
|
9756
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9757
10393
|
const content = this.textState.getText();
|
|
9758
|
-
const
|
|
9759
|
-
|
|
9760
|
-
|
|
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() });
|
|
9761
10399
|
}
|
|
9762
10400
|
/**
|
|
9763
|
-
* Indent the current paragraph
|
|
10401
|
+
* Indent the current paragraph(s) in selection.
|
|
9764
10402
|
*/
|
|
9765
10403
|
indentParagraph() {
|
|
9766
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9767
10404
|
const content = this.textState.getText();
|
|
9768
|
-
const
|
|
9769
|
-
|
|
9770
|
-
|
|
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() });
|
|
9771
10410
|
}
|
|
9772
10411
|
/**
|
|
9773
|
-
* Outdent the current paragraph
|
|
10412
|
+
* Outdent the current paragraph(s) in selection.
|
|
9774
10413
|
*/
|
|
9775
10414
|
outdentParagraph() {
|
|
9776
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9777
10415
|
const content = this.textState.getText();
|
|
9778
|
-
const
|
|
9779
|
-
|
|
9780
|
-
|
|
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() });
|
|
9781
10421
|
}
|
|
9782
10422
|
/**
|
|
9783
10423
|
* Get the list formatting for the current paragraph.
|
|
@@ -9939,6 +10579,79 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9939
10579
|
return result;
|
|
9940
10580
|
}
|
|
9941
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
|
+
// ============================================
|
|
9942
10655
|
// Serialization
|
|
9943
10656
|
// ============================================
|
|
9944
10657
|
/**
|
|
@@ -9992,6 +10705,8 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9992
10705
|
}));
|
|
9993
10706
|
// Serialize repeating sections
|
|
9994
10707
|
const repeatingSectionsData = this.repeatingSections.toJSON();
|
|
10708
|
+
// Serialize conditional sections
|
|
10709
|
+
const conditionalSectionsData = this.conditionalSections.toJSON();
|
|
9995
10710
|
// Serialize embedded objects
|
|
9996
10711
|
const embeddedObjects = [];
|
|
9997
10712
|
const objectsMap = this.embeddedObjects.getObjects();
|
|
@@ -10009,6 +10724,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10009
10724
|
paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
|
|
10010
10725
|
substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
|
|
10011
10726
|
repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
|
|
10727
|
+
conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
|
|
10012
10728
|
embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
|
|
10013
10729
|
hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
|
|
10014
10730
|
};
|
|
@@ -10047,6 +10763,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10047
10763
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10048
10764
|
content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
|
|
10049
10765
|
}
|
|
10766
|
+
// Restore conditional sections
|
|
10767
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10768
|
+
content.getConditionalSectionManager().fromJSON(data.conditionalSections);
|
|
10769
|
+
}
|
|
10050
10770
|
// Restore embedded objects using factory
|
|
10051
10771
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10052
10772
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10101,6 +10821,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10101
10821
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10102
10822
|
this.repeatingSections.fromJSON(data.repeatingSections);
|
|
10103
10823
|
}
|
|
10824
|
+
// Restore conditional sections
|
|
10825
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10826
|
+
this.conditionalSections.fromJSON(data.conditionalSections);
|
|
10827
|
+
}
|
|
10104
10828
|
// Restore embedded objects
|
|
10105
10829
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10106
10830
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10120,6 +10844,349 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10120
10844
|
}
|
|
10121
10845
|
}
|
|
10122
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
|
+
|
|
10123
11190
|
/**
|
|
10124
11191
|
* Abstract base class providing common functionality for regions.
|
|
10125
11192
|
*/
|
|
@@ -11155,6 +12222,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
|
|
|
11155
12222
|
const LOOP_LABEL_PADDING = 4;
|
|
11156
12223
|
const LOOP_LABEL_RADIUS = 4;
|
|
11157
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];
|
|
11158
12230
|
// Hyperlink styling
|
|
11159
12231
|
const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
|
|
11160
12232
|
class FlowingTextRenderer extends EventEmitter {
|
|
@@ -11412,8 +12484,6 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11412
12484
|
if (pageIndex === 0) {
|
|
11413
12485
|
// Clear table continuations when starting a new render cycle
|
|
11414
12486
|
this.clearTableContinuations();
|
|
11415
|
-
// Clear content hit targets - they will be re-registered during render
|
|
11416
|
-
this._hitTestManager.clearCategory('content');
|
|
11417
12487
|
// This is the first page, flow all text
|
|
11418
12488
|
const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
|
|
11419
12489
|
this.flowedPages.set(page.id, flowedPages);
|
|
@@ -11713,6 +12783,8 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11713
12783
|
const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
|
|
11714
12784
|
// Get hyperlinks for rendering
|
|
11715
12785
|
const hyperlinks = flowingContent.getAllHyperlinks();
|
|
12786
|
+
// Track relative objects to render after all lines (so they appear on top)
|
|
12787
|
+
const relativeObjects = [];
|
|
11716
12788
|
// Render each line
|
|
11717
12789
|
let y = bounds.y;
|
|
11718
12790
|
for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
|
|
@@ -11725,6 +12797,18 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11725
12797
|
if (clipToBounds && y > bounds.y + bounds.height) {
|
|
11726
12798
|
break;
|
|
11727
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
|
+
}
|
|
11728
12812
|
this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
|
|
11729
12813
|
y += line.height;
|
|
11730
12814
|
}
|
|
@@ -11735,6 +12819,10 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11735
12819
|
if (clipToBounds) {
|
|
11736
12820
|
ctx.restore();
|
|
11737
12821
|
}
|
|
12822
|
+
// Render relative objects on top of text (outside clip region)
|
|
12823
|
+
if (relativeObjects.length > 0) {
|
|
12824
|
+
this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
|
|
12825
|
+
}
|
|
11738
12826
|
}
|
|
11739
12827
|
/**
|
|
11740
12828
|
* Render selection highlight for a region.
|
|
@@ -13794,11 +14882,283 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13794
14882
|
verticalEndY = endInfo.y;
|
|
13795
14883
|
}
|
|
13796
14884
|
else if (endsAfterPage) {
|
|
13797
|
-
// Section continues to next page, end at bottom of content area
|
|
14885
|
+
// Section continues to next page, end at bottom of content area
|
|
14886
|
+
verticalEndY = contentBounds.y + contentBounds.height;
|
|
14887
|
+
}
|
|
14888
|
+
else {
|
|
14889
|
+
verticalEndY = verticalStartY; // No vertical line if neither start nor end
|
|
14890
|
+
}
|
|
14891
|
+
if (verticalEndY > verticalStartY) {
|
|
14892
|
+
ctx.beginPath();
|
|
14893
|
+
ctx.moveTo(connectorX, verticalStartY);
|
|
14894
|
+
ctx.lineTo(connectorX, verticalEndY);
|
|
14895
|
+
ctx.stroke();
|
|
14896
|
+
}
|
|
14897
|
+
// Draw "Loop" label last so it's in front of all lines
|
|
14898
|
+
if (hasStart) {
|
|
14899
|
+
const startY = startInfo.y;
|
|
14900
|
+
this.drawLoopLabel(ctx, labelX, startY - 10, 'Loop', isSelected);
|
|
14901
|
+
}
|
|
14902
|
+
// Update section's visual state
|
|
14903
|
+
section.visualState = {
|
|
14904
|
+
startPageIndex: hasStart ? pageIndex : -1,
|
|
14905
|
+
startY: hasStart ? startInfo.y : 0,
|
|
14906
|
+
endPageIndex: hasEnd ? pageIndex : -1,
|
|
14907
|
+
endY: hasEnd ? endInfo.y : 0,
|
|
14908
|
+
spansMultiplePages: !hasStart || !hasEnd
|
|
14909
|
+
};
|
|
14910
|
+
ctx.restore();
|
|
14911
|
+
}
|
|
14912
|
+
/**
|
|
14913
|
+
* Draw the "Loop" label in a rounded rectangle.
|
|
14914
|
+
* When not selected, draws an outlined rectangle.
|
|
14915
|
+
* When selected, draws a filled rectangle.
|
|
14916
|
+
*/
|
|
14917
|
+
drawLoopLabel(ctx, x, y, text, isSelected = false) {
|
|
14918
|
+
ctx.save();
|
|
14919
|
+
ctx.font = '10px Arial';
|
|
14920
|
+
const metrics = ctx.measureText(text);
|
|
14921
|
+
const textWidth = metrics.width;
|
|
14922
|
+
const textHeight = 10;
|
|
14923
|
+
const boxWidth = textWidth + LOOP_LABEL_PADDING * 2;
|
|
14924
|
+
const boxHeight = textHeight + LOOP_LABEL_PADDING * 2;
|
|
14925
|
+
ctx.beginPath();
|
|
14926
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, LOOP_LABEL_RADIUS);
|
|
14927
|
+
if (isSelected) {
|
|
14928
|
+
// Selected: filled background with white text
|
|
14929
|
+
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
14930
|
+
ctx.fill();
|
|
14931
|
+
ctx.fillStyle = '#ffffff';
|
|
14932
|
+
}
|
|
14933
|
+
else {
|
|
14934
|
+
// Not selected: white background, outlined with colored text
|
|
14935
|
+
ctx.fillStyle = '#ffffff';
|
|
14936
|
+
ctx.fill();
|
|
14937
|
+
ctx.strokeStyle = LOOP_INDICATOR_COLOR;
|
|
14938
|
+
ctx.lineWidth = 1.5;
|
|
14939
|
+
ctx.stroke();
|
|
14940
|
+
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
14941
|
+
}
|
|
14942
|
+
// Draw text
|
|
14943
|
+
ctx.textBaseline = 'middle';
|
|
14944
|
+
ctx.fillText(text, x + LOOP_LABEL_PADDING, y + boxHeight / 2);
|
|
14945
|
+
ctx.restore();
|
|
14946
|
+
}
|
|
14947
|
+
/**
|
|
14948
|
+
* Draw a rounded rectangle path.
|
|
14949
|
+
*/
|
|
14950
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
14951
|
+
ctx.moveTo(x + radius, y);
|
|
14952
|
+
ctx.lineTo(x + width - radius, y);
|
|
14953
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
14954
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
14955
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
14956
|
+
ctx.lineTo(x + radius, y + height);
|
|
14957
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
14958
|
+
ctx.lineTo(x, y + radius);
|
|
14959
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
14960
|
+
ctx.closePath();
|
|
14961
|
+
}
|
|
14962
|
+
/**
|
|
14963
|
+
* Find the Y position for a text index on a flowed page.
|
|
14964
|
+
* Returns the Y position at the TOP of the line containing the text index.
|
|
14965
|
+
*/
|
|
14966
|
+
findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
|
|
14967
|
+
let y = contentBounds.y;
|
|
14968
|
+
for (let i = 0; i < flowedPage.lines.length; i++) {
|
|
14969
|
+
const line = flowedPage.lines[i];
|
|
14970
|
+
// Check if this line contains the text index
|
|
14971
|
+
if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
|
|
14972
|
+
return { y, lineIndex: i };
|
|
14973
|
+
}
|
|
14974
|
+
// Check if text index is exactly at the start of this line
|
|
14975
|
+
// (for section boundaries that are at paragraph starts)
|
|
14976
|
+
if (textIndex === line.startIndex) {
|
|
14977
|
+
return { y, lineIndex: i };
|
|
14978
|
+
}
|
|
14979
|
+
y += line.height;
|
|
14980
|
+
}
|
|
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) {
|
|
13798
15158
|
verticalEndY = contentBounds.y + contentBounds.height;
|
|
13799
15159
|
}
|
|
13800
15160
|
else {
|
|
13801
|
-
verticalEndY = verticalStartY;
|
|
15161
|
+
verticalEndY = verticalStartY;
|
|
13802
15162
|
}
|
|
13803
15163
|
if (verticalEndY > verticalStartY) {
|
|
13804
15164
|
ctx.beginPath();
|
|
@@ -13806,12 +15166,12 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13806
15166
|
ctx.lineTo(connectorX, verticalEndY);
|
|
13807
15167
|
ctx.stroke();
|
|
13808
15168
|
}
|
|
13809
|
-
// Draw "
|
|
15169
|
+
// Draw "If" label
|
|
13810
15170
|
if (hasStart) {
|
|
13811
15171
|
const startY = startInfo.y;
|
|
13812
|
-
this.
|
|
15172
|
+
this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
|
|
13813
15173
|
}
|
|
13814
|
-
// Update
|
|
15174
|
+
// Update visual state
|
|
13815
15175
|
section.visualState = {
|
|
13816
15176
|
startPageIndex: hasStart ? pageIndex : -1,
|
|
13817
15177
|
startY: hasStart ? startInfo.y : 0,
|
|
@@ -13822,111 +15182,52 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13822
15182
|
ctx.restore();
|
|
13823
15183
|
}
|
|
13824
15184
|
/**
|
|
13825
|
-
* Draw the "
|
|
13826
|
-
* When not selected, draws an outlined rectangle.
|
|
13827
|
-
* When selected, draws a filled rectangle.
|
|
15185
|
+
* Draw the "If" label in a rounded rectangle.
|
|
13828
15186
|
*/
|
|
13829
|
-
|
|
15187
|
+
drawCondLabel(ctx, x, y, text, isSelected = false) {
|
|
13830
15188
|
ctx.save();
|
|
13831
15189
|
ctx.font = '10px Arial';
|
|
13832
15190
|
const metrics = ctx.measureText(text);
|
|
13833
15191
|
const textWidth = metrics.width;
|
|
13834
15192
|
const textHeight = 10;
|
|
13835
|
-
const boxWidth = textWidth +
|
|
13836
|
-
const boxHeight = textHeight +
|
|
15193
|
+
const boxWidth = textWidth + COND_LABEL_PADDING * 2;
|
|
15194
|
+
const boxHeight = textHeight + COND_LABEL_PADDING * 2;
|
|
13837
15195
|
ctx.beginPath();
|
|
13838
|
-
this.roundRect(ctx, x, y, boxWidth, boxHeight,
|
|
15196
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
|
|
13839
15197
|
if (isSelected) {
|
|
13840
|
-
|
|
13841
|
-
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
15198
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
13842
15199
|
ctx.fill();
|
|
13843
15200
|
ctx.fillStyle = '#ffffff';
|
|
13844
15201
|
}
|
|
13845
15202
|
else {
|
|
13846
|
-
// Not selected: white background, outlined with colored text
|
|
13847
15203
|
ctx.fillStyle = '#ffffff';
|
|
13848
15204
|
ctx.fill();
|
|
13849
|
-
ctx.strokeStyle =
|
|
15205
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
13850
15206
|
ctx.lineWidth = 1.5;
|
|
13851
15207
|
ctx.stroke();
|
|
13852
|
-
ctx.fillStyle =
|
|
15208
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
13853
15209
|
}
|
|
13854
|
-
// Draw text
|
|
13855
15210
|
ctx.textBaseline = 'middle';
|
|
13856
|
-
ctx.fillText(text, x +
|
|
15211
|
+
ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
|
|
13857
15212
|
ctx.restore();
|
|
13858
15213
|
}
|
|
13859
15214
|
/**
|
|
13860
|
-
*
|
|
13861
|
-
*/
|
|
13862
|
-
roundRect(ctx, x, y, width, height, radius) {
|
|
13863
|
-
ctx.moveTo(x + radius, y);
|
|
13864
|
-
ctx.lineTo(x + width - radius, y);
|
|
13865
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
13866
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
13867
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
13868
|
-
ctx.lineTo(x + radius, y + height);
|
|
13869
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
13870
|
-
ctx.lineTo(x, y + radius);
|
|
13871
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
13872
|
-
ctx.closePath();
|
|
13873
|
-
}
|
|
13874
|
-
/**
|
|
13875
|
-
* Find the Y position for a text index on a flowed page.
|
|
13876
|
-
* Returns the Y position at the TOP of the line containing the text index.
|
|
13877
|
-
*/
|
|
13878
|
-
findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
|
|
13879
|
-
let y = contentBounds.y;
|
|
13880
|
-
for (let i = 0; i < flowedPage.lines.length; i++) {
|
|
13881
|
-
const line = flowedPage.lines[i];
|
|
13882
|
-
// Check if this line contains the text index
|
|
13883
|
-
if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
|
|
13884
|
-
return { y, lineIndex: i };
|
|
13885
|
-
}
|
|
13886
|
-
// Check if text index is exactly at the start of this line
|
|
13887
|
-
// (for section boundaries that are at paragraph starts)
|
|
13888
|
-
if (textIndex === line.startIndex) {
|
|
13889
|
-
return { y, lineIndex: i };
|
|
13890
|
-
}
|
|
13891
|
-
y += line.height;
|
|
13892
|
-
}
|
|
13893
|
-
// Check if text index is just past the last line (end of content)
|
|
13894
|
-
if (flowedPage.lines.length > 0) {
|
|
13895
|
-
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
13896
|
-
if (textIndex === lastLine.endIndex + 1) {
|
|
13897
|
-
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
13898
|
-
}
|
|
13899
|
-
}
|
|
13900
|
-
return null;
|
|
13901
|
-
}
|
|
13902
|
-
/**
|
|
13903
|
-
* Check if a section spans across a flowed page (starts before and ends after).
|
|
13904
|
-
*/
|
|
13905
|
-
sectionSpansPage(section, flowedPage) {
|
|
13906
|
-
if (flowedPage.lines.length === 0)
|
|
13907
|
-
return false;
|
|
13908
|
-
const pageStart = flowedPage.startIndex;
|
|
13909
|
-
const pageEnd = flowedPage.endIndex;
|
|
13910
|
-
// Section spans this page if it started before and ends after
|
|
13911
|
-
return section.startIndex < pageStart && section.endIndex > pageEnd;
|
|
13912
|
-
}
|
|
13913
|
-
/**
|
|
13914
|
-
* Get a repeating section at a point (for click detection).
|
|
13915
|
-
* Checks if the point is on the Loop label or vertical connector.
|
|
15215
|
+
* Get a conditional section at a point (for click detection).
|
|
13916
15216
|
*/
|
|
13917
|
-
|
|
13918
|
-
const
|
|
13919
|
-
const
|
|
15217
|
+
getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
|
|
15218
|
+
const labelWidth = 22;
|
|
15219
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
13920
15220
|
const connectorX = labelX + labelWidth / 2;
|
|
13921
|
-
const hitRadius = 10;
|
|
15221
|
+
const hitRadius = 10;
|
|
13922
15222
|
for (const section of sections) {
|
|
13923
15223
|
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
13924
15224
|
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
13925
|
-
const sectionSpansThisPage =
|
|
15225
|
+
const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
|
|
15226
|
+
section.endIndex > flowedPage.endIndex;
|
|
13926
15227
|
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
13927
15228
|
continue;
|
|
13928
15229
|
}
|
|
13929
|
-
// Check if click is on the
|
|
15230
|
+
// Check if click is on the "If" label
|
|
13930
15231
|
if (startInfo) {
|
|
13931
15232
|
const labelY = startInfo.y - 10;
|
|
13932
15233
|
const labelHeight = 18;
|
|
@@ -13991,6 +15292,7 @@ class CanvasManager extends EventEmitter {
|
|
|
13991
15292
|
this.isSelectingText = false;
|
|
13992
15293
|
this.textSelectionStartPageId = null;
|
|
13993
15294
|
this.selectedSectionId = null;
|
|
15295
|
+
this.selectedConditionalSectionId = null;
|
|
13994
15296
|
this._activeSection = 'body';
|
|
13995
15297
|
this.lastClickTime = 0;
|
|
13996
15298
|
this.lastClickPosition = null;
|
|
@@ -14130,6 +15432,11 @@ class CanvasManager extends EventEmitter {
|
|
|
14130
15432
|
}
|
|
14131
15433
|
// 2. CONTENT: Render all text and elements
|
|
14132
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
|
+
}
|
|
14133
15440
|
// Render header content
|
|
14134
15441
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
14135
15442
|
this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
|
|
@@ -14157,6 +15464,16 @@ class CanvasManager extends EventEmitter {
|
|
|
14157
15464
|
this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
|
|
14158
15465
|
}
|
|
14159
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
|
+
}
|
|
14160
15477
|
// Render all elements (without selection marks)
|
|
14161
15478
|
this.renderPageElements(page, ctx);
|
|
14162
15479
|
// 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
|
|
@@ -14454,11 +15771,10 @@ class CanvasManager extends EventEmitter {
|
|
|
14454
15771
|
const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
|
|
14455
15772
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14456
15773
|
const object = embeddedObjectHit.data.object;
|
|
14457
|
-
//
|
|
15774
|
+
// If object is in a different section, switch to that section first
|
|
14458
15775
|
const objectSection = this.getSectionForEmbeddedObject(object);
|
|
14459
15776
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14460
|
-
|
|
14461
|
-
return;
|
|
15777
|
+
this.setActiveSection(objectSection);
|
|
14462
15778
|
}
|
|
14463
15779
|
// For relative-positioned objects, prepare for potential drag
|
|
14464
15780
|
// Don't start drag immediately - wait for threshold to allow double-click
|
|
@@ -14945,14 +16261,12 @@ class CanvasManager extends EventEmitter {
|
|
|
14945
16261
|
const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
|
|
14946
16262
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14947
16263
|
const clickedObject = embeddedObjectHit.data.object;
|
|
14948
|
-
//
|
|
16264
|
+
// If object is in a different section, switch to that section first
|
|
14949
16265
|
const objectSection = this.getSectionForEmbeddedObject(clickedObject);
|
|
14950
|
-
// Only allow selection if object is in the active section
|
|
14951
16266
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14952
|
-
|
|
14953
|
-
return;
|
|
16267
|
+
this.setActiveSection(objectSection);
|
|
14954
16268
|
}
|
|
14955
|
-
// Clicked on embedded object
|
|
16269
|
+
// Clicked on embedded object - clear text selection and select it
|
|
14956
16270
|
const activeFlowingContent = this.getFlowingContentForActiveSection();
|
|
14957
16271
|
if (activeFlowingContent) {
|
|
14958
16272
|
activeFlowingContent.clearSelection();
|
|
@@ -14989,6 +16303,64 @@ class CanvasManager extends EventEmitter {
|
|
|
14989
16303
|
}
|
|
14990
16304
|
}
|
|
14991
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
|
+
}
|
|
14992
16364
|
// If no regular element was clicked, try flowing text using unified region click handler
|
|
14993
16365
|
const ctx = this.contexts.get(pageId);
|
|
14994
16366
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
@@ -15198,26 +16570,21 @@ class CanvasManager extends EventEmitter {
|
|
|
15198
16570
|
const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
|
|
15199
16571
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
15200
16572
|
const object = embeddedObjectHit.data.object;
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
if (objectSection && objectSection !== this._activeSection) ;
|
|
15204
|
-
else {
|
|
15205
|
-
if (object.position === 'relative') {
|
|
15206
|
-
canvas.style.cursor = 'move';
|
|
15207
|
-
return;
|
|
15208
|
-
}
|
|
15209
|
-
// Show text cursor for objects in edit mode, arrow otherwise
|
|
15210
|
-
if (object instanceof TextBoxObject && this.editingTextBox === object) {
|
|
15211
|
-
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15212
|
-
}
|
|
15213
|
-
else if (object instanceof TableObject && this._focusedControl === object) {
|
|
15214
|
-
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15215
|
-
}
|
|
15216
|
-
else {
|
|
15217
|
-
canvas.style.cursor = 'default';
|
|
15218
|
-
}
|
|
16573
|
+
if (object.position === 'relative') {
|
|
16574
|
+
canvas.style.cursor = 'move';
|
|
15219
16575
|
return;
|
|
15220
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
|
+
}
|
|
16584
|
+
else {
|
|
16585
|
+
canvas.style.cursor = 'default';
|
|
16586
|
+
}
|
|
16587
|
+
return;
|
|
15221
16588
|
}
|
|
15222
16589
|
// Check for table cells (show text cursor)
|
|
15223
16590
|
const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
|
|
@@ -15407,6 +16774,7 @@ class CanvasManager extends EventEmitter {
|
|
|
15407
16774
|
});
|
|
15408
16775
|
this.selectedElements.clear();
|
|
15409
16776
|
this.selectedSectionId = null;
|
|
16777
|
+
this.selectedConditionalSectionId = null;
|
|
15410
16778
|
Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
|
|
15411
16779
|
this.render();
|
|
15412
16780
|
this.updateResizeHandleHitTargets();
|
|
@@ -16489,8 +17857,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
|
|
|
16489
17857
|
* - Repeating section indicators, loop markers
|
|
16490
17858
|
*/
|
|
16491
17859
|
class PDFGenerator {
|
|
16492
|
-
constructor() {
|
|
17860
|
+
constructor(fontManager) {
|
|
16493
17861
|
this.fontCache = new Map();
|
|
17862
|
+
this.customFontCache = new Map();
|
|
17863
|
+
this.fontManager = fontManager;
|
|
16494
17864
|
}
|
|
16495
17865
|
/**
|
|
16496
17866
|
* Generate a PDF from the document.
|
|
@@ -16501,9 +17871,13 @@ class PDFGenerator {
|
|
|
16501
17871
|
*/
|
|
16502
17872
|
async generate(document, flowedContent, _options) {
|
|
16503
17873
|
const pdfDoc = await pdfLib.PDFDocument.create();
|
|
17874
|
+
pdfDoc.registerFontkit(fontkit);
|
|
16504
17875
|
this.fontCache.clear();
|
|
17876
|
+
this.customFontCache.clear();
|
|
16505
17877
|
// Embed standard fonts we'll need
|
|
16506
17878
|
await this.embedStandardFonts(pdfDoc);
|
|
17879
|
+
// Embed any custom fonts that have font data
|
|
17880
|
+
await this.embedCustomFonts(pdfDoc);
|
|
16507
17881
|
// Render each page
|
|
16508
17882
|
for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
|
|
16509
17883
|
try {
|
|
@@ -16625,11 +17999,59 @@ class PDFGenerator {
|
|
|
16625
17999
|
}
|
|
16626
18000
|
return result;
|
|
16627
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
|
+
}
|
|
16628
18035
|
/**
|
|
16629
18036
|
* Get a font from cache by formatting style.
|
|
18037
|
+
* Checks custom fonts first, then falls back to standard fonts.
|
|
16630
18038
|
*/
|
|
16631
18039
|
getFont(formatting) {
|
|
16632
|
-
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);
|
|
16633
18055
|
return this.fontCache.get(standardFont) || this.fontCache.get(pdfLib.StandardFonts.Helvetica);
|
|
16634
18056
|
}
|
|
16635
18057
|
/**
|
|
@@ -16662,12 +18084,14 @@ class PDFGenerator {
|
|
|
16662
18084
|
for (const run of line.runs) {
|
|
16663
18085
|
if (!run.text)
|
|
16664
18086
|
continue;
|
|
16665
|
-
// Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
|
|
16666
|
-
const safeText = this.filterToWinAnsi(run.text);
|
|
16667
|
-
if (!safeText)
|
|
16668
|
-
continue;
|
|
16669
18087
|
// Ensure formatting has required properties with defaults
|
|
16670
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;
|
|
16671
18095
|
const font = this.getFont(formatting);
|
|
16672
18096
|
const fontSize = formatting.fontSize || 14;
|
|
16673
18097
|
const color = parseColor(formatting.color || '#000000');
|
|
@@ -21342,6 +22766,156 @@ class PDFImporter {
|
|
|
21342
22766
|
}
|
|
21343
22767
|
}
|
|
21344
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
|
+
|
|
21345
22919
|
class PCEditor extends EventEmitter {
|
|
21346
22920
|
constructor(container, options) {
|
|
21347
22921
|
super();
|
|
@@ -21368,7 +22942,8 @@ class PCEditor extends EventEmitter {
|
|
|
21368
22942
|
units: this.options.units
|
|
21369
22943
|
});
|
|
21370
22944
|
this.dataBinder = new DataBinder();
|
|
21371
|
-
this.
|
|
22945
|
+
this.fontManager = new FontManager();
|
|
22946
|
+
this.pdfGenerator = new PDFGenerator(this.fontManager);
|
|
21372
22947
|
this.clipboardManager = new ClipboardManager();
|
|
21373
22948
|
this.initialize();
|
|
21374
22949
|
}
|
|
@@ -21534,6 +23109,10 @@ class PCEditor extends EventEmitter {
|
|
|
21534
23109
|
this.canvasManager.on('table-cell-selection-changed', (data) => {
|
|
21535
23110
|
this.emit('table-cell-selection-changed', data);
|
|
21536
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
|
+
});
|
|
21537
23116
|
this.canvasManager.on('repeating-section-clicked', (data) => {
|
|
21538
23117
|
// Repeating section clicked - update selection state
|
|
21539
23118
|
if (data.section && data.section.id) {
|
|
@@ -21544,6 +23123,16 @@ class PCEditor extends EventEmitter {
|
|
|
21544
23123
|
this.emitSelectionChange();
|
|
21545
23124
|
}
|
|
21546
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
|
+
});
|
|
21547
23136
|
// Listen for section focus changes from CanvasManager (double-click)
|
|
21548
23137
|
this.canvasManager.on('section-focus-changed', (data) => {
|
|
21549
23138
|
// Update our internal state to match the canvas manager
|
|
@@ -22372,17 +23961,24 @@ class PCEditor extends EventEmitter {
|
|
|
22372
23961
|
this.selectAll();
|
|
22373
23962
|
return;
|
|
22374
23963
|
}
|
|
22375
|
-
// If an embedded object is selected (but not being edited),
|
|
22376
|
-
|
|
22377
|
-
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
22378
|
-
if (isArrowKey && this.canvasManager.hasSelectedElements()) {
|
|
22379
|
-
// 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()) {
|
|
22380
23966
|
const editingTextBox = this.canvasManager.getEditingTextBox();
|
|
22381
23967
|
const focusedTable = this.canvasManager.getFocusedControl();
|
|
22382
23968
|
const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
|
|
22383
23969
|
if (!isEditing) {
|
|
22384
|
-
//
|
|
22385
|
-
|
|
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
|
+
}
|
|
22386
23982
|
}
|
|
22387
23983
|
}
|
|
22388
23984
|
// Use the unified focus system to get the currently focused control
|
|
@@ -22485,6 +24081,32 @@ class PCEditor extends EventEmitter {
|
|
|
22485
24081
|
this.canvasManager.clearSelection();
|
|
22486
24082
|
this.canvasManager.render();
|
|
22487
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
|
+
}
|
|
22488
24110
|
/**
|
|
22489
24111
|
* Find embedded object info by ID across all flowing content sources.
|
|
22490
24112
|
*/
|
|
@@ -23602,11 +25224,17 @@ class PCEditor extends EventEmitter {
|
|
|
23602
25224
|
let totalFieldCount = 0;
|
|
23603
25225
|
// Step 1: Expand repeating sections in body (header/footer don't support them)
|
|
23604
25226
|
this.expandRepeatingSections(bodyContent, data);
|
|
23605
|
-
// 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
|
|
23606
25230
|
this.expandTableRowLoops(bodyContent, data);
|
|
23607
25231
|
this.expandTableRowLoops(this.document.headerFlowingContent, data);
|
|
23608
25232
|
this.expandTableRowLoops(this.document.footerFlowingContent, data);
|
|
23609
|
-
// 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
|
|
23610
25238
|
totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
|
|
23611
25239
|
// Step 4: Substitute all fields in embedded objects in body
|
|
23612
25240
|
totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
|
|
@@ -23809,14 +25437,67 @@ class PCEditor extends EventEmitter {
|
|
|
23809
25437
|
}
|
|
23810
25438
|
}
|
|
23811
25439
|
}
|
|
23812
|
-
// Rewrite field names in the original (first) iteration to use index 0
|
|
23813
|
-
for (const field of fieldsInSection) {
|
|
23814
|
-
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
23815
|
-
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
25440
|
+
// Rewrite field names in the original (first) iteration to use index 0
|
|
25441
|
+
for (const field of fieldsInSection) {
|
|
25442
|
+
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
25443
|
+
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
25444
|
+
}
|
|
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);
|
|
23816
25496
|
}
|
|
23817
|
-
// Remove the
|
|
23818
|
-
|
|
25497
|
+
// Remove the conditional marker regardless
|
|
25498
|
+
table.removeRowConditional(cond.id);
|
|
23819
25499
|
}
|
|
25500
|
+
table.markLayoutDirty();
|
|
23820
25501
|
}
|
|
23821
25502
|
/**
|
|
23822
25503
|
* Get a value at a path without array defaulting.
|
|
@@ -24065,7 +25746,7 @@ class PCEditor extends EventEmitter {
|
|
|
24065
25746
|
toggleBulletList() {
|
|
24066
25747
|
if (!this._isReady)
|
|
24067
25748
|
return;
|
|
24068
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25749
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24069
25750
|
if (!flowingContent)
|
|
24070
25751
|
return;
|
|
24071
25752
|
flowingContent.toggleBulletList();
|
|
@@ -24078,7 +25759,7 @@ class PCEditor extends EventEmitter {
|
|
|
24078
25759
|
toggleNumberedList() {
|
|
24079
25760
|
if (!this._isReady)
|
|
24080
25761
|
return;
|
|
24081
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25762
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24082
25763
|
if (!flowingContent)
|
|
24083
25764
|
return;
|
|
24084
25765
|
flowingContent.toggleNumberedList();
|
|
@@ -24091,7 +25772,7 @@ class PCEditor extends EventEmitter {
|
|
|
24091
25772
|
indentParagraph() {
|
|
24092
25773
|
if (!this._isReady)
|
|
24093
25774
|
return;
|
|
24094
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25775
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24095
25776
|
if (!flowingContent)
|
|
24096
25777
|
return;
|
|
24097
25778
|
flowingContent.indentParagraph();
|
|
@@ -24104,7 +25785,7 @@ class PCEditor extends EventEmitter {
|
|
|
24104
25785
|
outdentParagraph() {
|
|
24105
25786
|
if (!this._isReady)
|
|
24106
25787
|
return;
|
|
24107
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25788
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24108
25789
|
if (!flowingContent)
|
|
24109
25790
|
return;
|
|
24110
25791
|
flowingContent.outdentParagraph();
|
|
@@ -24117,7 +25798,7 @@ class PCEditor extends EventEmitter {
|
|
|
24117
25798
|
getListFormatting() {
|
|
24118
25799
|
if (!this._isReady)
|
|
24119
25800
|
return undefined;
|
|
24120
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25801
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24121
25802
|
if (!flowingContent)
|
|
24122
25803
|
return undefined;
|
|
24123
25804
|
return flowingContent.getListFormatting();
|
|
@@ -24328,9 +26009,12 @@ class PCEditor extends EventEmitter {
|
|
|
24328
26009
|
// If a table is focused, create a row loop instead of a text repeating section
|
|
24329
26010
|
const focusedTable = this.getFocusedTable();
|
|
24330
26011
|
if (focusedTable && focusedTable.focusedCell) {
|
|
24331
|
-
|
|
24332
|
-
const
|
|
24333
|
-
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);
|
|
24334
26018
|
if (loop) {
|
|
24335
26019
|
this.canvasManager.render();
|
|
24336
26020
|
this.emit('table-row-loop-added', { table: focusedTable, loop });
|
|
@@ -24402,6 +26086,103 @@ class PCEditor extends EventEmitter {
|
|
|
24402
26086
|
return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
|
|
24403
26087
|
}
|
|
24404
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
|
+
// ============================================
|
|
24405
26186
|
// Header/Footer API
|
|
24406
26187
|
// ============================================
|
|
24407
26188
|
/**
|
|
@@ -24752,6 +26533,39 @@ class PCEditor extends EventEmitter {
|
|
|
24752
26533
|
setLogging(enabled) {
|
|
24753
26534
|
Logger.setEnabled(enabled);
|
|
24754
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
|
+
}
|
|
24755
26569
|
destroy() {
|
|
24756
26570
|
this.disableTextInput();
|
|
24757
26571
|
if (this.canvasManager) {
|
|
@@ -26268,7 +28082,7 @@ class MergeDataPane extends BasePane {
|
|
|
26268
28082
|
createContent() {
|
|
26269
28083
|
const container = document.createElement('div');
|
|
26270
28084
|
// Textarea for JSON
|
|
26271
|
-
const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
|
|
28085
|
+
const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
|
|
26272
28086
|
container.appendChild(textareaGroup);
|
|
26273
28087
|
// Error hint (hidden by default)
|
|
26274
28088
|
this.errorHint = this.createHint('');
|
|
@@ -26414,17 +28228,29 @@ class FormattingPane extends BasePane {
|
|
|
26414
28228
|
attach(options) {
|
|
26415
28229
|
super.attach(options);
|
|
26416
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
|
+
}
|
|
26417
28236
|
// Update on cursor/selection changes
|
|
26418
28237
|
const updateHandler = () => this.updateFromEditor();
|
|
26419
28238
|
this.editor.on('cursor-changed', updateHandler);
|
|
26420
28239
|
this.editor.on('selection-changed', updateHandler);
|
|
26421
28240
|
this.editor.on('text-changed', updateHandler);
|
|
26422
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);
|
|
26423
28248
|
this.eventCleanup.push(() => {
|
|
26424
28249
|
this.editor?.off('cursor-changed', updateHandler);
|
|
26425
28250
|
this.editor?.off('selection-changed', updateHandler);
|
|
26426
28251
|
this.editor?.off('text-changed', updateHandler);
|
|
26427
28252
|
this.editor?.off('formatting-changed', updateHandler);
|
|
28253
|
+
this.editor?.off('font-registered', fontHandler);
|
|
26428
28254
|
});
|
|
26429
28255
|
// Initial update
|
|
26430
28256
|
this.updateFromEditor();
|
|
@@ -26493,38 +28319,82 @@ class FormattingPane extends BasePane {
|
|
|
26493
28319
|
listsGroup.appendChild(this.outdentBtn);
|
|
26494
28320
|
listsSection.appendChild(listsGroup);
|
|
26495
28321
|
container.appendChild(listsSection);
|
|
26496
|
-
// Font section
|
|
28322
|
+
// Font section - label-value grid with right-aligned labels
|
|
26497
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:';
|
|
26498
28330
|
this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
|
|
26499
28331
|
this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
|
|
26500
|
-
|
|
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:';
|
|
26501
28339
|
this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
|
|
26502
28340
|
this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
|
|
26503
|
-
|
|
28341
|
+
fontGrid.appendChild(sizeLabel);
|
|
28342
|
+
fontGrid.appendChild(this.fontSizeSelect);
|
|
28343
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28344
|
+
fontSection.appendChild(fontGrid);
|
|
26504
28345
|
container.appendChild(fontSection);
|
|
26505
|
-
// Color section
|
|
28346
|
+
// Color section - label-value grid with right-aligned labels
|
|
26506
28347
|
const colorSection = this.createSection('Color');
|
|
26507
|
-
const
|
|
26508
|
-
|
|
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:';
|
|
26509
28354
|
this.colorInput = this.createColorInput('#000000');
|
|
26510
28355
|
this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
|
|
26511
|
-
|
|
26512
|
-
|
|
26513
|
-
|
|
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:';
|
|
26514
28363
|
this.highlightInput = this.createColorInput('#ffff00');
|
|
26515
28364
|
this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
|
|
26516
|
-
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);
|
|
26517
28370
|
const clearHighlightBtn = this.createButton('Clear');
|
|
26518
28371
|
clearHighlightBtn.className = 'pc-pane-button';
|
|
26519
|
-
clearHighlightBtn.style.marginLeft = '4px';
|
|
26520
28372
|
this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
|
|
26521
|
-
|
|
26522
|
-
|
|
26523
|
-
|
|
26524
|
-
|
|
28373
|
+
highlightControls.appendChild(clearHighlightBtn);
|
|
28374
|
+
colorGrid.appendChild(highlightLabel);
|
|
28375
|
+
colorGrid.appendChild(highlightControls);
|
|
28376
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28377
|
+
colorSection.appendChild(colorGrid);
|
|
26525
28378
|
container.appendChild(colorSection);
|
|
26526
28379
|
return container;
|
|
26527
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
|
+
}
|
|
26528
28398
|
updateFromEditor() {
|
|
26529
28399
|
if (!this.editor)
|
|
26530
28400
|
return;
|
|
@@ -26748,10 +28618,10 @@ class HyperlinkPane extends BasePane {
|
|
|
26748
28618
|
const container = document.createElement('div');
|
|
26749
28619
|
// URL input
|
|
26750
28620
|
this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
|
|
26751
|
-
container.appendChild(this.createFormGroup('URL', this.urlInput));
|
|
28621
|
+
container.appendChild(this.createFormGroup('URL:', this.urlInput));
|
|
26752
28622
|
// Title input
|
|
26753
28623
|
this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
|
|
26754
|
-
container.appendChild(this.createFormGroup('Title', this.titleInput));
|
|
28624
|
+
container.appendChild(this.createFormGroup('Title:', this.titleInput));
|
|
26755
28625
|
// Apply button
|
|
26756
28626
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
26757
28627
|
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
@@ -26902,10 +28772,10 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26902
28772
|
const container = document.createElement('div');
|
|
26903
28773
|
// Field name input
|
|
26904
28774
|
this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
|
|
26905
|
-
container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
|
|
28775
|
+
container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
|
|
26906
28776
|
// Default value input
|
|
26907
28777
|
this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
|
|
26908
|
-
container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
|
|
28778
|
+
container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
|
|
26909
28779
|
// Value type select
|
|
26910
28780
|
this.valueTypeSelect = this.createSelect([
|
|
26911
28781
|
{ value: '', label: '(None)' },
|
|
@@ -26914,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26914
28784
|
{ value: 'date', label: 'Date' }
|
|
26915
28785
|
]);
|
|
26916
28786
|
this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
|
|
26917
|
-
container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
|
|
28787
|
+
container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
|
|
26918
28788
|
// Number format group
|
|
26919
28789
|
this.numberFormatGroup = this.createSection();
|
|
26920
28790
|
this.numberFormatGroup.style.display = 'none';
|
|
@@ -26924,7 +28794,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26924
28794
|
{ value: '0,0', label: 'Thousands separator (0,0)' },
|
|
26925
28795
|
{ value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
|
|
26926
28796
|
]);
|
|
26927
|
-
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
|
|
28797
|
+
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
|
|
26928
28798
|
container.appendChild(this.numberFormatGroup);
|
|
26929
28799
|
// Currency format group
|
|
26930
28800
|
this.currencyFormatGroup = this.createSection();
|
|
@@ -26935,7 +28805,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26935
28805
|
{ value: 'GBP', label: 'GBP' },
|
|
26936
28806
|
{ value: 'JPY', label: 'JPY' }
|
|
26937
28807
|
]);
|
|
26938
|
-
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
|
|
28808
|
+
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
|
|
26939
28809
|
container.appendChild(this.currencyFormatGroup);
|
|
26940
28810
|
// Date format group
|
|
26941
28811
|
this.dateFormatGroup = this.createSection();
|
|
@@ -26946,7 +28816,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26946
28816
|
{ value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
|
|
26947
28817
|
{ value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
|
|
26948
28818
|
]);
|
|
26949
|
-
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
|
|
28819
|
+
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
|
|
26950
28820
|
container.appendChild(this.dateFormatGroup);
|
|
26951
28821
|
// Apply button
|
|
26952
28822
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
@@ -27108,12 +28978,17 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27108
28978
|
if (this.editor) {
|
|
27109
28979
|
// Listen for repeating section selection
|
|
27110
28980
|
const selectionHandler = (event) => {
|
|
27111
|
-
|
|
27112
|
-
|
|
28981
|
+
const sel = event.selection || event;
|
|
28982
|
+
if (sel.type === 'repeating-section' && sel.sectionId) {
|
|
28983
|
+
const section = this.editor?.getRepeatingSection(sel.sectionId);
|
|
27113
28984
|
if (section) {
|
|
27114
28985
|
this.showSection(section);
|
|
27115
28986
|
}
|
|
27116
28987
|
}
|
|
28988
|
+
else {
|
|
28989
|
+
// Selection changed away from repeating section — hide pane
|
|
28990
|
+
this.hideSection();
|
|
28991
|
+
}
|
|
27117
28992
|
};
|
|
27118
28993
|
const removedHandler = () => {
|
|
27119
28994
|
this.hideSection();
|
|
@@ -27130,7 +29005,7 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27130
29005
|
const container = document.createElement('div');
|
|
27131
29006
|
// Field path input
|
|
27132
29007
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27133
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29008
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27134
29009
|
hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
|
|
27135
29010
|
}));
|
|
27136
29011
|
// Apply button
|
|
@@ -27230,6 +29105,158 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27230
29105
|
}
|
|
27231
29106
|
}
|
|
27232
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
|
+
|
|
27233
29260
|
/**
|
|
27234
29261
|
* TableRowLoopPane - Edit table row loop properties.
|
|
27235
29262
|
*
|
|
@@ -27254,14 +29281,28 @@ class TableRowLoopPane extends BasePane {
|
|
|
27254
29281
|
}
|
|
27255
29282
|
attach(options) {
|
|
27256
29283
|
super.attach(options);
|
|
27257
|
-
|
|
27258
|
-
|
|
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
|
+
}
|
|
27259
29300
|
}
|
|
27260
29301
|
createContent() {
|
|
27261
29302
|
const container = document.createElement('div');
|
|
27262
29303
|
// Field path input
|
|
27263
29304
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27264
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29305
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27265
29306
|
hint: 'Path to array in merge data (e.g., "items" or "orders")'
|
|
27266
29307
|
}));
|
|
27267
29308
|
// Apply button
|
|
@@ -27421,56 +29462,63 @@ class TextBoxPane extends BasePane {
|
|
|
27421
29462
|
}
|
|
27422
29463
|
createContent() {
|
|
27423
29464
|
const container = document.createElement('div');
|
|
27424
|
-
// Position section
|
|
29465
|
+
// Position section - Type on same row as label
|
|
27425
29466
|
const positionSection = this.createSection('Position');
|
|
27426
29467
|
this.positionSelect = this.createSelect([
|
|
27427
29468
|
{ value: 'inline', label: 'Inline' },
|
|
27428
29469
|
{ value: 'block', label: 'Block' },
|
|
27429
29470
|
{ value: 'relative', label: 'Relative' }
|
|
27430
29471
|
], 'inline');
|
|
27431
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27432
|
-
|
|
29472
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29473
|
+
this.updateOffsetVisibility();
|
|
29474
|
+
this.applyChanges();
|
|
29475
|
+
});
|
|
29476
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27433
29477
|
// Offset group (only visible for relative positioning)
|
|
27434
29478
|
this.offsetGroup = document.createElement('div');
|
|
27435
29479
|
this.offsetGroup.style.display = 'none';
|
|
27436
29480
|
const offsetRow = this.createRow();
|
|
27437
29481
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27438
29482
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27439
|
-
|
|
27440
|
-
|
|
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 }));
|
|
27441
29487
|
this.offsetGroup.appendChild(offsetRow);
|
|
27442
29488
|
positionSection.appendChild(this.offsetGroup);
|
|
27443
29489
|
container.appendChild(positionSection);
|
|
27444
|
-
// Background
|
|
27445
|
-
const bgSection = this.createSection(
|
|
29490
|
+
// Background - color on same row as label
|
|
29491
|
+
const bgSection = this.createSection();
|
|
27446
29492
|
this.bgColorInput = this.createColorInput('#ffffff');
|
|
27447
|
-
|
|
29493
|
+
this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
|
|
29494
|
+
bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
|
|
27448
29495
|
container.appendChild(bgSection);
|
|
27449
29496
|
// Border section
|
|
27450
29497
|
const borderSection = this.createSection('Border');
|
|
27451
29498
|
const borderRow = this.createRow();
|
|
27452
29499
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
|
|
27453
29500
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27454
|
-
|
|
27455
|
-
|
|
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 }));
|
|
27456
29505
|
borderSection.appendChild(borderRow);
|
|
29506
|
+
// Border style on same row as label
|
|
27457
29507
|
this.borderStyleSelect = this.createSelect([
|
|
27458
29508
|
{ value: 'solid', label: 'Solid' },
|
|
27459
29509
|
{ value: 'dashed', label: 'Dashed' },
|
|
27460
29510
|
{ value: 'dotted', label: 'Dotted' },
|
|
27461
29511
|
{ value: 'none', label: 'None' }
|
|
27462
29512
|
], 'solid');
|
|
27463
|
-
|
|
29513
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
|
|
29514
|
+
borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27464
29515
|
container.appendChild(borderSection);
|
|
27465
|
-
// Padding
|
|
27466
|
-
const paddingSection = this.createSection(
|
|
29516
|
+
// Padding on same row as label
|
|
29517
|
+
const paddingSection = this.createSection();
|
|
27467
29518
|
this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
|
|
27468
|
-
|
|
29519
|
+
this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
|
|
29520
|
+
paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
|
|
27469
29521
|
container.appendChild(paddingSection);
|
|
27470
|
-
// Apply button
|
|
27471
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27472
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27473
|
-
container.appendChild(applyBtn);
|
|
27474
29522
|
return container;
|
|
27475
29523
|
}
|
|
27476
29524
|
updateFromSelection() {
|
|
@@ -27546,7 +29594,6 @@ class TextBoxPane extends BasePane {
|
|
|
27546
29594
|
}
|
|
27547
29595
|
applyChanges() {
|
|
27548
29596
|
if (!this.editor || !this.currentTextBox) {
|
|
27549
|
-
this.onApplyCallback?.(false, new Error('No text box selected'));
|
|
27550
29597
|
return;
|
|
27551
29598
|
}
|
|
27552
29599
|
const updates = {};
|
|
@@ -27582,12 +29629,7 @@ class TextBoxPane extends BasePane {
|
|
|
27582
29629
|
}
|
|
27583
29630
|
try {
|
|
27584
29631
|
const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
|
|
27585
|
-
|
|
27586
|
-
this.onApplyCallback?.(true);
|
|
27587
|
-
}
|
|
27588
|
-
else {
|
|
27589
|
-
this.onApplyCallback?.(false, new Error('Failed to update text box'));
|
|
27590
|
-
}
|
|
29632
|
+
this.onApplyCallback?.(success);
|
|
27591
29633
|
}
|
|
27592
29634
|
catch (error) {
|
|
27593
29635
|
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -27663,28 +29705,35 @@ class ImagePane extends BasePane {
|
|
|
27663
29705
|
}
|
|
27664
29706
|
createContent() {
|
|
27665
29707
|
const container = document.createElement('div');
|
|
27666
|
-
// Position section
|
|
29708
|
+
// Position section — with heading, matching TextBoxPane
|
|
27667
29709
|
const positionSection = this.createSection('Position');
|
|
27668
29710
|
this.positionSelect = this.createSelect([
|
|
27669
29711
|
{ value: 'inline', label: 'Inline' },
|
|
27670
29712
|
{ value: 'block', label: 'Block' },
|
|
27671
29713
|
{ value: 'relative', label: 'Relative' }
|
|
27672
29714
|
], 'inline');
|
|
27673
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27674
|
-
|
|
29715
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29716
|
+
this.updateOffsetVisibility();
|
|
29717
|
+
this.applyChanges();
|
|
29718
|
+
});
|
|
29719
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27675
29720
|
// Offset group (only visible for relative positioning)
|
|
27676
29721
|
this.offsetGroup = document.createElement('div');
|
|
27677
29722
|
this.offsetGroup.style.display = 'none';
|
|
27678
29723
|
const offsetRow = this.createRow();
|
|
27679
29724
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27680
29725
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27681
|
-
|
|
27682
|
-
|
|
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 }));
|
|
27683
29730
|
this.offsetGroup.appendChild(offsetRow);
|
|
27684
29731
|
positionSection.appendChild(this.offsetGroup);
|
|
27685
29732
|
container.appendChild(positionSection);
|
|
27686
|
-
|
|
27687
|
-
|
|
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';
|
|
27688
29737
|
this.fitModeSelect = this.createSelect([
|
|
27689
29738
|
{ value: 'contain', label: 'Contain' },
|
|
27690
29739
|
{ value: 'cover', label: 'Cover' },
|
|
@@ -27692,34 +29741,31 @@ class ImagePane extends BasePane {
|
|
|
27692
29741
|
{ value: 'none', label: 'None (original size)' },
|
|
27693
29742
|
{ value: 'tile', label: 'Tile' }
|
|
27694
29743
|
], 'contain');
|
|
27695
|
-
|
|
29744
|
+
this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
|
|
29745
|
+
displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
|
|
27696
29746
|
this.resizeModeSelect = this.createSelect([
|
|
27697
29747
|
{ value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
|
|
27698
29748
|
{ value: 'free', label: 'Free Resize' }
|
|
27699
29749
|
], 'locked-aspect-ratio');
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
|
|
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
|
|
27704
29755
|
this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
|
|
27705
|
-
|
|
27706
|
-
container.appendChild(
|
|
27707
|
-
|
|
27708
|
-
|
|
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
|
|
27709
29760
|
this.fileInput = document.createElement('input');
|
|
27710
29761
|
this.fileInput.type = 'file';
|
|
27711
29762
|
this.fileInput.accept = 'image/*';
|
|
27712
29763
|
this.fileInput.style.display = 'none';
|
|
27713
29764
|
this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
|
|
27714
|
-
|
|
29765
|
+
container.appendChild(this.fileInput);
|
|
27715
29766
|
const changeSourceBtn = this.createButton('Change Image...');
|
|
27716
29767
|
this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
|
|
27717
|
-
|
|
27718
|
-
container.appendChild(sourceSection);
|
|
27719
|
-
// Apply button
|
|
27720
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27721
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27722
|
-
container.appendChild(applyBtn);
|
|
29768
|
+
container.appendChild(changeSourceBtn);
|
|
27723
29769
|
return container;
|
|
27724
29770
|
}
|
|
27725
29771
|
updateFromSelection() {
|
|
@@ -27938,60 +29984,57 @@ class TablePane extends BasePane {
|
|
|
27938
29984
|
const container = document.createElement('div');
|
|
27939
29985
|
// Structure section
|
|
27940
29986
|
const structureSection = this.createSection('Structure');
|
|
29987
|
+
// Rows/Columns info with aligned labels
|
|
27941
29988
|
const structureInfo = document.createElement('div');
|
|
27942
|
-
structureInfo.className = 'pc-pane-info
|
|
29989
|
+
structureInfo.className = 'pc-pane-table-structure-info';
|
|
27943
29990
|
this.rowCountDisplay = document.createElement('span');
|
|
29991
|
+
this.rowCountDisplay.className = 'pc-pane-info-value';
|
|
27944
29992
|
this.colCountDisplay = document.createElement('span');
|
|
27945
|
-
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
rowInfo.appendChild(this.rowCountDisplay);
|
|
27949
|
-
const colInfo = document.createElement('div');
|
|
27950
|
-
colInfo.className = 'pc-pane-info';
|
|
27951
|
-
colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
|
|
27952
|
-
colInfo.appendChild(this.colCountDisplay);
|
|
27953
|
-
structureInfo.appendChild(rowInfo);
|
|
27954
|
-
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 }));
|
|
27955
29996
|
structureSection.appendChild(structureInfo);
|
|
27956
|
-
// Row
|
|
27957
|
-
const
|
|
29997
|
+
// Row buttons
|
|
29998
|
+
const rowBtns = this.createButtonGroup();
|
|
27958
29999
|
const addRowBtn = this.createButton('+ Row');
|
|
27959
30000
|
this.addButtonListener(addRowBtn, () => this.insertRow());
|
|
27960
30001
|
const removeRowBtn = this.createButton('- Row');
|
|
27961
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();
|
|
27962
30008
|
const addColBtn = this.createButton('+ Column');
|
|
27963
30009
|
this.addButtonListener(addColBtn, () => this.insertColumn());
|
|
27964
30010
|
const removeColBtn = this.createButton('- Column');
|
|
27965
30011
|
this.addButtonListener(removeColBtn, () => this.removeColumn());
|
|
27966
|
-
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
structureSection.appendChild(
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
const headersSection = this.createSection('Headers');
|
|
27974
|
-
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';
|
|
27975
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 }));
|
|
27976
30022
|
this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
headersSection.appendChild(applyHeadersBtn);
|
|
27983
|
-
container.appendChild(headersSection);
|
|
27984
|
-
// 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)
|
|
27985
30028
|
const defaultsSection = this.createSection('Defaults');
|
|
27986
|
-
const
|
|
30029
|
+
const defaultsGroup = document.createElement('div');
|
|
30030
|
+
defaultsGroup.className = 'pc-pane-table-defaults';
|
|
27987
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 }));
|
|
27988
30034
|
this.defaultBorderColorInput = this.createColorInput('#cccccc');
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
defaultsSection.appendChild(
|
|
27992
|
-
const applyDefaultsBtn = this.createButton('Apply Defaults');
|
|
27993
|
-
this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
|
|
27994
|
-
defaultsSection.appendChild(applyDefaultsBtn);
|
|
30035
|
+
this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
|
|
30036
|
+
defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
|
|
30037
|
+
defaultsSection.appendChild(defaultsGroup);
|
|
27995
30038
|
container.appendChild(defaultsSection);
|
|
27996
30039
|
// Cell formatting section
|
|
27997
30040
|
const cellSection = this.createSection('Cell Formatting');
|
|
@@ -28008,9 +30051,11 @@ class TablePane extends BasePane {
|
|
|
28008
30051
|
mergeBtnGroup.appendChild(this.mergeCellsBtn);
|
|
28009
30052
|
mergeBtnGroup.appendChild(this.splitCellBtn);
|
|
28010
30053
|
cellSection.appendChild(mergeBtnGroup);
|
|
28011
|
-
|
|
30054
|
+
cellSection.appendChild(document.createElement('hr'));
|
|
30055
|
+
// Background — inline
|
|
28012
30056
|
this.cellBgColorInput = this.createColorInput('#ffffff');
|
|
28013
|
-
|
|
30057
|
+
this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
|
|
30058
|
+
cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
|
|
28014
30059
|
// Border checkboxes
|
|
28015
30060
|
const borderChecks = document.createElement('div');
|
|
28016
30061
|
borderChecks.className = 'pc-pane-row';
|
|
@@ -28042,24 +30087,29 @@ class TablePane extends BasePane {
|
|
|
28042
30087
|
checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
|
|
28043
30088
|
if (checkLabels[3])
|
|
28044
30089
|
checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
|
|
28045
|
-
|
|
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));
|
|
28046
30095
|
// Border properties
|
|
28047
30096
|
const borderPropsRow = this.createRow();
|
|
28048
30097
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
|
|
28049
30098
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
28050
|
-
|
|
28051
|
-
|
|
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 }));
|
|
28052
30103
|
cellSection.appendChild(borderPropsRow);
|
|
30104
|
+
// Style — inline
|
|
28053
30105
|
this.borderStyleSelect = this.createSelect([
|
|
28054
30106
|
{ value: 'solid', label: 'Solid' },
|
|
28055
30107
|
{ value: 'dashed', label: 'Dashed' },
|
|
28056
30108
|
{ value: 'dotted', label: 'Dotted' },
|
|
28057
30109
|
{ value: 'none', label: 'None' }
|
|
28058
30110
|
], 'solid');
|
|
28059
|
-
|
|
28060
|
-
|
|
28061
|
-
this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
|
|
28062
|
-
cellSection.appendChild(applyCellBtn);
|
|
30111
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
|
|
30112
|
+
cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
28063
30113
|
container.appendChild(cellSection);
|
|
28064
30114
|
return container;
|
|
28065
30115
|
}
|
|
@@ -28319,6 +30369,8 @@ exports.BasePane = BasePane;
|
|
|
28319
30369
|
exports.BaseTextRegion = BaseTextRegion;
|
|
28320
30370
|
exports.BodyTextRegion = BodyTextRegion;
|
|
28321
30371
|
exports.ClipboardManager = ClipboardManager;
|
|
30372
|
+
exports.ConditionalSectionManager = ConditionalSectionManager;
|
|
30373
|
+
exports.ConditionalSectionPane = ConditionalSectionPane;
|
|
28322
30374
|
exports.ContentAnalyzer = ContentAnalyzer;
|
|
28323
30375
|
exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
|
|
28324
30376
|
exports.Document = Document;
|
|
@@ -28329,6 +30381,7 @@ exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
|
|
|
28329
30381
|
exports.EmbeddedObjectManager = EmbeddedObjectManager;
|
|
28330
30382
|
exports.EventEmitter = EventEmitter;
|
|
28331
30383
|
exports.FlowingTextContent = FlowingTextContent;
|
|
30384
|
+
exports.FontManager = FontManager;
|
|
28332
30385
|
exports.FooterTextRegion = FooterTextRegion;
|
|
28333
30386
|
exports.FormattingPane = FormattingPane;
|
|
28334
30387
|
exports.HeaderTextRegion = HeaderTextRegion;
|
|
@@ -28344,6 +30397,7 @@ exports.PDFImportError = PDFImportError;
|
|
|
28344
30397
|
exports.PDFImporter = PDFImporter;
|
|
28345
30398
|
exports.PDFParser = PDFParser;
|
|
28346
30399
|
exports.Page = Page;
|
|
30400
|
+
exports.PredicateEvaluator = PredicateEvaluator;
|
|
28347
30401
|
exports.RegionManager = RegionManager;
|
|
28348
30402
|
exports.RepeatingSectionManager = RepeatingSectionManager;
|
|
28349
30403
|
exports.RepeatingSectionPane = RepeatingSectionPane;
|