@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.esm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StandardFonts, rgb, PDFDocument } from 'pdf-lib';
|
|
2
|
+
import fontkit from '@pdf-lib/fontkit';
|
|
2
3
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
3
4
|
|
|
4
5
|
class EventEmitter {
|
|
@@ -2286,6 +2287,291 @@ class RepeatingSectionManager extends EventEmitter {
|
|
|
2286
2287
|
}
|
|
2287
2288
|
}
|
|
2288
2289
|
|
|
2290
|
+
/**
|
|
2291
|
+
* Manages conditional sections within text content.
|
|
2292
|
+
* Conditional sections define ranges of content that are shown or hidden
|
|
2293
|
+
* based on a boolean predicate evaluated against merge data.
|
|
2294
|
+
* They start and end at paragraph boundaries.
|
|
2295
|
+
*/
|
|
2296
|
+
class ConditionalSectionManager extends EventEmitter {
|
|
2297
|
+
constructor() {
|
|
2298
|
+
super();
|
|
2299
|
+
this.sections = new Map();
|
|
2300
|
+
this.nextId = 1;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Create a new conditional section.
|
|
2304
|
+
* @param startIndex Text index at paragraph start (must be 0 or immediately after a newline)
|
|
2305
|
+
* @param endIndex Text index at closing paragraph start (must be immediately after a newline)
|
|
2306
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
2307
|
+
*/
|
|
2308
|
+
create(startIndex, endIndex, predicate) {
|
|
2309
|
+
const id = `cond-${this.nextId++}`;
|
|
2310
|
+
const section = {
|
|
2311
|
+
id,
|
|
2312
|
+
predicate,
|
|
2313
|
+
startIndex,
|
|
2314
|
+
endIndex
|
|
2315
|
+
};
|
|
2316
|
+
this.sections.set(id, section);
|
|
2317
|
+
this.emit('section-added', { section });
|
|
2318
|
+
return section;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Remove a conditional section by ID.
|
|
2322
|
+
*/
|
|
2323
|
+
remove(id) {
|
|
2324
|
+
const section = this.sections.get(id);
|
|
2325
|
+
if (section) {
|
|
2326
|
+
this.sections.delete(id);
|
|
2327
|
+
this.emit('section-removed', { section });
|
|
2328
|
+
}
|
|
2329
|
+
return section;
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Get a conditional section by ID.
|
|
2333
|
+
*/
|
|
2334
|
+
getSection(id) {
|
|
2335
|
+
return this.sections.get(id);
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Get all conditional sections.
|
|
2339
|
+
*/
|
|
2340
|
+
getSections() {
|
|
2341
|
+
return Array.from(this.sections.values());
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Get all conditional sections sorted by startIndex.
|
|
2345
|
+
*/
|
|
2346
|
+
getSectionsSorted() {
|
|
2347
|
+
return this.getSections().sort((a, b) => a.startIndex - b.startIndex);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Get all conditional sections sorted by startIndex in descending order.
|
|
2351
|
+
* Useful for processing sections end-to-start during merge.
|
|
2352
|
+
*/
|
|
2353
|
+
getSectionsDescending() {
|
|
2354
|
+
return this.getSections().sort((a, b) => b.startIndex - a.startIndex);
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Find a conditional section that contains the given text index.
|
|
2358
|
+
*/
|
|
2359
|
+
getSectionContaining(textIndex) {
|
|
2360
|
+
for (const section of this.sections.values()) {
|
|
2361
|
+
if (textIndex >= section.startIndex && textIndex < section.endIndex) {
|
|
2362
|
+
return section;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return undefined;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
2369
|
+
* Returns the section if textIndex matches startIndex or endIndex.
|
|
2370
|
+
*/
|
|
2371
|
+
getSectionAtBoundary(textIndex) {
|
|
2372
|
+
for (const section of this.sections.values()) {
|
|
2373
|
+
if (section.startIndex === textIndex || section.endIndex === textIndex) {
|
|
2374
|
+
return section;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return undefined;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Update a section's predicate.
|
|
2381
|
+
*/
|
|
2382
|
+
updatePredicate(id, predicate) {
|
|
2383
|
+
const section = this.sections.get(id);
|
|
2384
|
+
if (!section) {
|
|
2385
|
+
return false;
|
|
2386
|
+
}
|
|
2387
|
+
section.predicate = predicate;
|
|
2388
|
+
this.emit('section-updated', { section });
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Update a section's visual state (called during rendering).
|
|
2393
|
+
*/
|
|
2394
|
+
updateVisualState(id, visualState) {
|
|
2395
|
+
const section = this.sections.get(id);
|
|
2396
|
+
if (!section) {
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
section.visualState = visualState;
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Shift section positions when text is inserted.
|
|
2404
|
+
* @param fromIndex The position where text was inserted
|
|
2405
|
+
* @param delta The number of characters inserted (positive)
|
|
2406
|
+
*/
|
|
2407
|
+
shiftSections(fromIndex, delta) {
|
|
2408
|
+
let changed = false;
|
|
2409
|
+
for (const section of this.sections.values()) {
|
|
2410
|
+
if (fromIndex <= section.startIndex) {
|
|
2411
|
+
section.startIndex += delta;
|
|
2412
|
+
section.endIndex += delta;
|
|
2413
|
+
changed = true;
|
|
2414
|
+
}
|
|
2415
|
+
else if (fromIndex < section.endIndex) {
|
|
2416
|
+
section.endIndex += delta;
|
|
2417
|
+
changed = true;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (changed) {
|
|
2421
|
+
this.emit('sections-shifted', { fromIndex, delta });
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Handle deletion of text range.
|
|
2426
|
+
* Sections entirely within the deleted range are removed.
|
|
2427
|
+
* Sections partially overlapping are adjusted or removed.
|
|
2428
|
+
* @returns Array of removed sections
|
|
2429
|
+
*/
|
|
2430
|
+
handleDeletion(start, length) {
|
|
2431
|
+
const end = start + length;
|
|
2432
|
+
const removedSections = [];
|
|
2433
|
+
const sectionsToUpdate = [];
|
|
2434
|
+
for (const section of this.sections.values()) {
|
|
2435
|
+
if (section.startIndex >= start && section.endIndex <= end) {
|
|
2436
|
+
removedSections.push(section);
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (section.startIndex < end && section.endIndex > start) {
|
|
2440
|
+
if (start <= section.startIndex) {
|
|
2441
|
+
removedSections.push(section);
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
if (start < section.endIndex) {
|
|
2445
|
+
if (end >= section.endIndex) {
|
|
2446
|
+
const newEnd = start;
|
|
2447
|
+
if (newEnd <= section.startIndex) {
|
|
2448
|
+
removedSections.push(section);
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
sectionsToUpdate.push({
|
|
2452
|
+
id: section.id,
|
|
2453
|
+
newStart: section.startIndex,
|
|
2454
|
+
newEnd: newEnd
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
const newEnd = section.endIndex - length;
|
|
2459
|
+
sectionsToUpdate.push({
|
|
2460
|
+
id: section.id,
|
|
2461
|
+
newStart: section.startIndex,
|
|
2462
|
+
newEnd: newEnd
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (section.startIndex >= end) {
|
|
2469
|
+
sectionsToUpdate.push({
|
|
2470
|
+
id: section.id,
|
|
2471
|
+
newStart: section.startIndex - length,
|
|
2472
|
+
newEnd: section.endIndex - length
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
for (const section of removedSections) {
|
|
2477
|
+
this.sections.delete(section.id);
|
|
2478
|
+
this.emit('section-removed', { section });
|
|
2479
|
+
}
|
|
2480
|
+
for (const update of sectionsToUpdate) {
|
|
2481
|
+
const section = this.sections.get(update.id);
|
|
2482
|
+
if (section) {
|
|
2483
|
+
section.startIndex = update.newStart;
|
|
2484
|
+
section.endIndex = update.newEnd;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (removedSections.length > 0 || sectionsToUpdate.length > 0) {
|
|
2488
|
+
this.emit('sections-changed');
|
|
2489
|
+
}
|
|
2490
|
+
return removedSections;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Validate that the given boundaries are at paragraph boundaries.
|
|
2494
|
+
* Also checks that conditionals don't partially overlap repeating sections.
|
|
2495
|
+
* @param start The proposed start index
|
|
2496
|
+
* @param end The proposed end index
|
|
2497
|
+
* @param content The text content to validate against
|
|
2498
|
+
* @returns true if valid, false otherwise
|
|
2499
|
+
*/
|
|
2500
|
+
validateBoundaries(start, end, content) {
|
|
2501
|
+
if (start !== 0 && content[start - 1] !== '\n') {
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
if (end !== 0 && end < content.length && content[end - 1] !== '\n') {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
if (end <= start) {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
// Check for overlapping conditional sections
|
|
2511
|
+
for (const existing of this.sections.values()) {
|
|
2512
|
+
if ((start >= existing.startIndex && start < existing.endIndex) ||
|
|
2513
|
+
(end > existing.startIndex && end <= existing.endIndex) ||
|
|
2514
|
+
(start <= existing.startIndex && end >= existing.endIndex)) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Get the number of conditional sections.
|
|
2522
|
+
*/
|
|
2523
|
+
get count() {
|
|
2524
|
+
return this.sections.size;
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Check if there are any conditional sections.
|
|
2528
|
+
*/
|
|
2529
|
+
get isEmpty() {
|
|
2530
|
+
return this.sections.size === 0;
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Clear all conditional sections.
|
|
2534
|
+
*/
|
|
2535
|
+
clear() {
|
|
2536
|
+
const hadSections = this.sections.size > 0;
|
|
2537
|
+
this.sections.clear();
|
|
2538
|
+
if (hadSections) {
|
|
2539
|
+
this.emit('sections-cleared');
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Serialize all sections to JSON.
|
|
2544
|
+
*/
|
|
2545
|
+
toJSON() {
|
|
2546
|
+
return this.getSectionsSorted().map(section => ({
|
|
2547
|
+
id: section.id,
|
|
2548
|
+
predicate: section.predicate,
|
|
2549
|
+
startIndex: section.startIndex,
|
|
2550
|
+
endIndex: section.endIndex
|
|
2551
|
+
}));
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Load sections from serialized data.
|
|
2555
|
+
*/
|
|
2556
|
+
fromJSON(data) {
|
|
2557
|
+
this.clear();
|
|
2558
|
+
for (const sectionData of data) {
|
|
2559
|
+
const section = {
|
|
2560
|
+
id: sectionData.id,
|
|
2561
|
+
predicate: sectionData.predicate,
|
|
2562
|
+
startIndex: sectionData.startIndex,
|
|
2563
|
+
endIndex: sectionData.endIndex
|
|
2564
|
+
};
|
|
2565
|
+
this.sections.set(section.id, section);
|
|
2566
|
+
const idNum = parseInt(sectionData.id.replace('cond-', ''), 10);
|
|
2567
|
+
if (!isNaN(idNum) && idNum >= this.nextId) {
|
|
2568
|
+
this.nextId = idNum + 1;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
this.emit('sections-loaded', { count: this.sections.size });
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2289
2575
|
/**
|
|
2290
2576
|
* HyperlinkManager - Manages hyperlinks within flowing text content
|
|
2291
2577
|
*/
|
|
@@ -6191,12 +6477,20 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6191
6477
|
this._coveredCells = new Map();
|
|
6192
6478
|
// Row loops for merge expansion
|
|
6193
6479
|
this._rowLoops = new Map();
|
|
6480
|
+
// Row conditionals for conditional display
|
|
6481
|
+
this._rowConditionals = new Map();
|
|
6194
6482
|
// Layout caching for performance
|
|
6195
6483
|
this._layoutDirty = true;
|
|
6196
6484
|
this._cachedRowHeights = [];
|
|
6197
6485
|
this._cachedRowPositions = [];
|
|
6198
6486
|
// Multi-page rendering info: pageIndex -> slice render info
|
|
6199
6487
|
this._renderedSlices = new Map();
|
|
6488
|
+
// ============================================
|
|
6489
|
+
// Table Row Conditionals
|
|
6490
|
+
// ============================================
|
|
6491
|
+
this._nextCondId = 1;
|
|
6492
|
+
this._selectedRowLoopId = null;
|
|
6493
|
+
this._selectedRowConditionalId = null;
|
|
6200
6494
|
// Tables ONLY support block positioning - force it regardless of config
|
|
6201
6495
|
this._position = 'block';
|
|
6202
6496
|
// Initialize defaults
|
|
@@ -6890,6 +7184,85 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6890
7184
|
loopRangesOverlap(start1, end1, start2, end2) {
|
|
6891
7185
|
return start1 <= end2 && start2 <= end1;
|
|
6892
7186
|
}
|
|
7187
|
+
generateRowConditionalId() {
|
|
7188
|
+
return `row-cond-${this._nextCondId++}`;
|
|
7189
|
+
}
|
|
7190
|
+
/**
|
|
7191
|
+
* Create a row conditional.
|
|
7192
|
+
*/
|
|
7193
|
+
createRowConditional(startRowIndex, endRowIndex, predicate) {
|
|
7194
|
+
if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
|
|
7195
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Invalid row range');
|
|
7196
|
+
return null;
|
|
7197
|
+
}
|
|
7198
|
+
if (startRowIndex > endRowIndex) {
|
|
7199
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Start index must be <= end index');
|
|
7200
|
+
return null;
|
|
7201
|
+
}
|
|
7202
|
+
// Check for overlapping conditionals
|
|
7203
|
+
for (const existing of this._rowConditionals.values()) {
|
|
7204
|
+
if (this.loopRangesOverlap(startRowIndex, endRowIndex, existing.startRowIndex, existing.endRowIndex)) {
|
|
7205
|
+
Logger.warn('[pc-editor:TableObject.createRowConditional] Range overlaps with existing conditional');
|
|
7206
|
+
return null;
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
const cond = {
|
|
7210
|
+
id: this.generateRowConditionalId(),
|
|
7211
|
+
predicate,
|
|
7212
|
+
startRowIndex,
|
|
7213
|
+
endRowIndex
|
|
7214
|
+
};
|
|
7215
|
+
this._rowConditionals.set(cond.id, cond);
|
|
7216
|
+
this.emit('row-conditional-created', { conditional: cond });
|
|
7217
|
+
this.emit('content-changed', {});
|
|
7218
|
+
return cond;
|
|
7219
|
+
}
|
|
7220
|
+
/**
|
|
7221
|
+
* Remove a row conditional by ID.
|
|
7222
|
+
*/
|
|
7223
|
+
removeRowConditional(id) {
|
|
7224
|
+
const cond = this._rowConditionals.get(id);
|
|
7225
|
+
if (!cond)
|
|
7226
|
+
return false;
|
|
7227
|
+
this._rowConditionals.delete(id);
|
|
7228
|
+
this.emit('row-conditional-removed', { conditionalId: id });
|
|
7229
|
+
return true;
|
|
7230
|
+
}
|
|
7231
|
+
/**
|
|
7232
|
+
* Get a row conditional by ID.
|
|
7233
|
+
*/
|
|
7234
|
+
getRowConditional(id) {
|
|
7235
|
+
return this._rowConditionals.get(id);
|
|
7236
|
+
}
|
|
7237
|
+
/**
|
|
7238
|
+
* Get all row conditionals.
|
|
7239
|
+
*/
|
|
7240
|
+
getAllRowConditionals() {
|
|
7241
|
+
return Array.from(this._rowConditionals.values());
|
|
7242
|
+
}
|
|
7243
|
+
/**
|
|
7244
|
+
* Get the row conditional at a given row index.
|
|
7245
|
+
*/
|
|
7246
|
+
getRowConditionalAtRow(rowIndex) {
|
|
7247
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7248
|
+
if (rowIndex >= cond.startRowIndex && rowIndex <= cond.endRowIndex) {
|
|
7249
|
+
return cond;
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
return undefined;
|
|
7253
|
+
}
|
|
7254
|
+
/**
|
|
7255
|
+
* Update a row conditional's predicate.
|
|
7256
|
+
*/
|
|
7257
|
+
updateRowConditionalPredicate(id, predicate) {
|
|
7258
|
+
const cond = this._rowConditionals.get(id);
|
|
7259
|
+
if (!cond)
|
|
7260
|
+
return false;
|
|
7261
|
+
cond.predicate = predicate;
|
|
7262
|
+
this.emit('row-conditional-updated', { conditional: cond });
|
|
7263
|
+
this.emit('content-changed', {});
|
|
7264
|
+
return true;
|
|
7265
|
+
}
|
|
6893
7266
|
/**
|
|
6894
7267
|
* Shift row loop indices when rows are inserted or removed.
|
|
6895
7268
|
* @param fromIndex The row index where insertion/removal occurred
|
|
@@ -6930,6 +7303,41 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
6930
7303
|
this._rowLoops.delete(id);
|
|
6931
7304
|
this.emit('row-loop-removed', { loopId: id, reason: 'row-deleted' });
|
|
6932
7305
|
}
|
|
7306
|
+
// Also shift row conditional indices
|
|
7307
|
+
this.shiftRowConditionalIndices(fromIndex, delta);
|
|
7308
|
+
}
|
|
7309
|
+
/**
|
|
7310
|
+
* Shift row conditional indices when rows are inserted or removed.
|
|
7311
|
+
*/
|
|
7312
|
+
shiftRowConditionalIndices(fromIndex, delta) {
|
|
7313
|
+
const condsToRemove = [];
|
|
7314
|
+
for (const cond of this._rowConditionals.values()) {
|
|
7315
|
+
if (delta < 0) {
|
|
7316
|
+
const removeCount = Math.abs(delta);
|
|
7317
|
+
const removeEnd = fromIndex + removeCount - 1;
|
|
7318
|
+
if (fromIndex <= cond.endRowIndex && removeEnd >= cond.startRowIndex) {
|
|
7319
|
+
condsToRemove.push(cond.id);
|
|
7320
|
+
continue;
|
|
7321
|
+
}
|
|
7322
|
+
if (fromIndex < cond.startRowIndex) {
|
|
7323
|
+
cond.startRowIndex += delta;
|
|
7324
|
+
cond.endRowIndex += delta;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
else {
|
|
7328
|
+
if (fromIndex <= cond.startRowIndex) {
|
|
7329
|
+
cond.startRowIndex += delta;
|
|
7330
|
+
cond.endRowIndex += delta;
|
|
7331
|
+
}
|
|
7332
|
+
else if (fromIndex <= cond.endRowIndex) {
|
|
7333
|
+
cond.endRowIndex += delta;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
for (const id of condsToRemove) {
|
|
7338
|
+
this._rowConditionals.delete(id);
|
|
7339
|
+
this.emit('row-conditional-removed', { conditionalId: id, reason: 'row-deleted' });
|
|
7340
|
+
}
|
|
6933
7341
|
}
|
|
6934
7342
|
/**
|
|
6935
7343
|
* Get rows in a range (for loop expansion).
|
|
@@ -7830,6 +8238,10 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7830
8238
|
if (this._rowLoops.size > 0) {
|
|
7831
8239
|
this.renderRowLoopIndicators(ctx);
|
|
7832
8240
|
}
|
|
8241
|
+
// Render row conditional indicators
|
|
8242
|
+
if (this._rowConditionals.size > 0) {
|
|
8243
|
+
this.renderRowConditionalIndicators(ctx);
|
|
8244
|
+
}
|
|
7833
8245
|
// Render cell range selection highlight
|
|
7834
8246
|
if (this._selectedRange) {
|
|
7835
8247
|
this.renderRangeSelection(ctx);
|
|
@@ -7844,11 +8256,54 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7844
8256
|
}
|
|
7845
8257
|
}
|
|
7846
8258
|
/**
|
|
7847
|
-
*
|
|
8259
|
+
* Select a row loop by ID (for pane display).
|
|
8260
|
+
*/
|
|
8261
|
+
selectRowLoop(loopId) {
|
|
8262
|
+
this._selectedRowLoopId = loopId;
|
|
8263
|
+
}
|
|
8264
|
+
/**
|
|
8265
|
+
* Get the currently selected row loop ID.
|
|
8266
|
+
*/
|
|
8267
|
+
get selectedRowLoopId() {
|
|
8268
|
+
return this._selectedRowLoopId;
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Hit-test a point against row loop labels.
|
|
8272
|
+
* Point should be in table-local coordinates.
|
|
8273
|
+
* Returns the loop if a label was clicked, null otherwise.
|
|
7848
8274
|
*/
|
|
8275
|
+
getRowLoopAtPoint(point) {
|
|
8276
|
+
let rowPositions = this._cachedRowPositions;
|
|
8277
|
+
if (rowPositions.length === 0) {
|
|
8278
|
+
rowPositions = [];
|
|
8279
|
+
let y = 0;
|
|
8280
|
+
for (const row of this._rows) {
|
|
8281
|
+
rowPositions.push(y);
|
|
8282
|
+
y += row.calculatedHeight;
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
for (const loop of this._rowLoops.values()) {
|
|
8286
|
+
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
8287
|
+
let _endY = startY;
|
|
8288
|
+
for (let i = loop.startRowIndex; i <= loop.endRowIndex && i < this._rows.length; i++) {
|
|
8289
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8290
|
+
}
|
|
8291
|
+
// Label bounds (matches rendering)
|
|
8292
|
+
const labelWidth = 30; // approximate for "Loop" at 10px
|
|
8293
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8294
|
+
const labelX = -6 - labelWidth - 4;
|
|
8295
|
+
const labelY = startY - labelHeight - 2;
|
|
8296
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8297
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8298
|
+
return loop;
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
return null;
|
|
8302
|
+
}
|
|
7849
8303
|
renderRowLoopIndicators(ctx) {
|
|
7850
|
-
const
|
|
7851
|
-
const
|
|
8304
|
+
const color = TableObject.LOOP_COLOR;
|
|
8305
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8306
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
7852
8307
|
// Calculate row Y positions if not cached
|
|
7853
8308
|
let rowPositions = this._cachedRowPositions;
|
|
7854
8309
|
if (rowPositions.length === 0) {
|
|
@@ -7859,12 +8314,8 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7859
8314
|
y += row.calculatedHeight;
|
|
7860
8315
|
}
|
|
7861
8316
|
}
|
|
7862
|
-
// Colors for different loops (cycle through these)
|
|
7863
|
-
const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
|
|
7864
|
-
let colorIndex = 0;
|
|
7865
8317
|
for (const loop of this._rowLoops.values()) {
|
|
7866
|
-
const
|
|
7867
|
-
colorIndex++;
|
|
8318
|
+
const isSelected = this._selectedRowLoopId === loop.id;
|
|
7868
8319
|
// Calculate the Y range for this loop
|
|
7869
8320
|
const startY = rowPositions[loop.startRowIndex] || 0;
|
|
7870
8321
|
let endY = startY;
|
|
@@ -7874,31 +8325,149 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
7874
8325
|
const loopHeight = endY - startY;
|
|
7875
8326
|
// Draw colored stripe on left side
|
|
7876
8327
|
ctx.fillStyle = color;
|
|
7877
|
-
ctx.fillRect(-
|
|
7878
|
-
// Draw
|
|
8328
|
+
ctx.fillRect(-6, startY, 4, loopHeight);
|
|
8329
|
+
// Draw vertical connector line
|
|
8330
|
+
ctx.strokeStyle = color;
|
|
8331
|
+
ctx.lineWidth = 1;
|
|
8332
|
+
ctx.beginPath();
|
|
8333
|
+
ctx.moveTo(-4, startY);
|
|
8334
|
+
ctx.lineTo(-4, endY);
|
|
8335
|
+
ctx.stroke();
|
|
8336
|
+
// Draw "Loop" label — matches text flow style
|
|
7879
8337
|
ctx.save();
|
|
7880
8338
|
ctx.font = '10px Arial';
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
const
|
|
7884
|
-
const
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
ctx.
|
|
8339
|
+
const labelText = 'Loop';
|
|
8340
|
+
const metrics = ctx.measureText(labelText);
|
|
8341
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8342
|
+
const boxHeight = 10 + padding * 2;
|
|
8343
|
+
const labelX = -6 - boxWidth - 4;
|
|
8344
|
+
const labelY = startY - boxHeight - 2;
|
|
8345
|
+
ctx.beginPath();
|
|
8346
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8347
|
+
if (isSelected) {
|
|
8348
|
+
// Selected: filled background with white text
|
|
8349
|
+
ctx.fillStyle = color;
|
|
8350
|
+
ctx.fill();
|
|
8351
|
+
ctx.fillStyle = '#ffffff';
|
|
8352
|
+
}
|
|
8353
|
+
else {
|
|
8354
|
+
// Not selected: white background, outlined with colored text
|
|
8355
|
+
ctx.fillStyle = '#ffffff';
|
|
8356
|
+
ctx.fill();
|
|
8357
|
+
ctx.strokeStyle = color;
|
|
8358
|
+
ctx.lineWidth = 1.5;
|
|
8359
|
+
ctx.stroke();
|
|
8360
|
+
ctx.fillStyle = color;
|
|
8361
|
+
}
|
|
8362
|
+
ctx.textBaseline = 'middle';
|
|
8363
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
7888
8364
|
ctx.restore();
|
|
7889
|
-
|
|
8365
|
+
}
|
|
8366
|
+
}
|
|
8367
|
+
/**
|
|
8368
|
+
* Select a row conditional by ID (for pane display).
|
|
8369
|
+
*/
|
|
8370
|
+
selectRowConditional(conditionalId) {
|
|
8371
|
+
this._selectedRowConditionalId = conditionalId;
|
|
8372
|
+
}
|
|
8373
|
+
/**
|
|
8374
|
+
* Get the currently selected row conditional ID.
|
|
8375
|
+
*/
|
|
8376
|
+
get selectedRowConditionalId() {
|
|
8377
|
+
return this._selectedRowConditionalId;
|
|
8378
|
+
}
|
|
8379
|
+
/**
|
|
8380
|
+
* Hit-test a point against row conditional labels.
|
|
8381
|
+
* Point should be in table-local coordinates.
|
|
8382
|
+
*/
|
|
8383
|
+
getRowConditionalAtPoint(point) {
|
|
8384
|
+
let rowPositions = this._cachedRowPositions;
|
|
8385
|
+
if (rowPositions.length === 0) {
|
|
8386
|
+
rowPositions = [];
|
|
8387
|
+
let y = 0;
|
|
8388
|
+
for (const row of this._rows) {
|
|
8389
|
+
rowPositions.push(y);
|
|
8390
|
+
y += row.calculatedHeight;
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8394
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8395
|
+
let _endY = startY;
|
|
8396
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8397
|
+
_endY += this._rows[i].calculatedHeight;
|
|
8398
|
+
}
|
|
8399
|
+
// Label bounds (right side of table, offset from loop labels)
|
|
8400
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8401
|
+
const labelWidth = 22; // approximate for "If" at 10px
|
|
8402
|
+
const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
|
|
8403
|
+
const labelX = totalWidth + 10;
|
|
8404
|
+
const labelY = startY - labelHeight - 2;
|
|
8405
|
+
if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
|
|
8406
|
+
point.y >= labelY && point.y <= labelY + labelHeight + 4) {
|
|
8407
|
+
return cond;
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
return null;
|
|
8411
|
+
}
|
|
8412
|
+
renderRowConditionalIndicators(ctx) {
|
|
8413
|
+
const color = TableObject.COND_COLOR;
|
|
8414
|
+
const padding = TableObject.LOOP_LABEL_PADDING;
|
|
8415
|
+
const radius = TableObject.LOOP_LABEL_RADIUS;
|
|
8416
|
+
let rowPositions = this._cachedRowPositions;
|
|
8417
|
+
if (rowPositions.length === 0) {
|
|
8418
|
+
rowPositions = [];
|
|
8419
|
+
let y = 0;
|
|
8420
|
+
for (const row of this._rows) {
|
|
8421
|
+
rowPositions.push(y);
|
|
8422
|
+
y += row.calculatedHeight;
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
|
|
8426
|
+
for (const cond of this._rowConditionals.values()) {
|
|
8427
|
+
const isSelected = this._selectedRowConditionalId === cond.id;
|
|
8428
|
+
const startY = rowPositions[cond.startRowIndex] || 0;
|
|
8429
|
+
let endY = startY;
|
|
8430
|
+
for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
|
|
8431
|
+
endY += this._rows[i].calculatedHeight;
|
|
8432
|
+
}
|
|
8433
|
+
const condHeight = endY - startY;
|
|
8434
|
+
// Draw colored stripe on right side
|
|
8435
|
+
ctx.fillStyle = color;
|
|
8436
|
+
ctx.fillRect(totalWidth + 2, startY, 4, condHeight);
|
|
8437
|
+
// Draw vertical connector line
|
|
7890
8438
|
ctx.strokeStyle = color;
|
|
7891
8439
|
ctx.lineWidth = 1;
|
|
7892
8440
|
ctx.beginPath();
|
|
7893
|
-
|
|
7894
|
-
ctx.
|
|
7895
|
-
ctx.lineTo(-indicatorWidth - 6, startY);
|
|
7896
|
-
ctx.lineTo(-indicatorWidth - 6, startY + 6);
|
|
7897
|
-
// Bottom bracket
|
|
7898
|
-
ctx.moveTo(-indicatorWidth - 2, endY);
|
|
7899
|
-
ctx.lineTo(-indicatorWidth - 6, endY);
|
|
7900
|
-
ctx.lineTo(-indicatorWidth - 6, endY - 6);
|
|
8441
|
+
ctx.moveTo(totalWidth + 4, startY);
|
|
8442
|
+
ctx.lineTo(totalWidth + 4, endY);
|
|
7901
8443
|
ctx.stroke();
|
|
8444
|
+
// Draw "If" label
|
|
8445
|
+
ctx.save();
|
|
8446
|
+
ctx.font = '10px Arial';
|
|
8447
|
+
const labelText = 'If';
|
|
8448
|
+
const metrics = ctx.measureText(labelText);
|
|
8449
|
+
const boxWidth = metrics.width + padding * 2;
|
|
8450
|
+
const boxHeight = 10 + padding * 2;
|
|
8451
|
+
const labelX = totalWidth + 10;
|
|
8452
|
+
const labelY = startY - boxHeight - 2;
|
|
8453
|
+
ctx.beginPath();
|
|
8454
|
+
ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
|
|
8455
|
+
if (isSelected) {
|
|
8456
|
+
ctx.fillStyle = color;
|
|
8457
|
+
ctx.fill();
|
|
8458
|
+
ctx.fillStyle = '#ffffff';
|
|
8459
|
+
}
|
|
8460
|
+
else {
|
|
8461
|
+
ctx.fillStyle = '#ffffff';
|
|
8462
|
+
ctx.fill();
|
|
8463
|
+
ctx.strokeStyle = color;
|
|
8464
|
+
ctx.lineWidth = 1.5;
|
|
8465
|
+
ctx.stroke();
|
|
8466
|
+
ctx.fillStyle = color;
|
|
8467
|
+
}
|
|
8468
|
+
ctx.textBaseline = 'middle';
|
|
8469
|
+
ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
|
|
8470
|
+
ctx.restore();
|
|
7902
8471
|
}
|
|
7903
8472
|
}
|
|
7904
8473
|
/**
|
|
@@ -8017,6 +8586,14 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8017
8586
|
columns: this._columns.map(col => ({ ...col })),
|
|
8018
8587
|
rows: this._rows.map(row => row.toData()),
|
|
8019
8588
|
rowLoops,
|
|
8589
|
+
rowConditionals: this._rowConditionals.size > 0
|
|
8590
|
+
? Array.from(this._rowConditionals.values()).map(c => ({
|
|
8591
|
+
id: c.id,
|
|
8592
|
+
predicate: c.predicate,
|
|
8593
|
+
startRowIndex: c.startRowIndex,
|
|
8594
|
+
endRowIndex: c.endRowIndex
|
|
8595
|
+
}))
|
|
8596
|
+
: undefined,
|
|
8020
8597
|
defaultCellPadding: this._defaultCellPadding,
|
|
8021
8598
|
defaultBorderColor: this._defaultBorderColor,
|
|
8022
8599
|
defaultBorderWidth: this._defaultBorderWidth,
|
|
@@ -8060,6 +8637,17 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8060
8637
|
});
|
|
8061
8638
|
}
|
|
8062
8639
|
}
|
|
8640
|
+
// Load row conditionals if present
|
|
8641
|
+
if (data.data.rowConditionals) {
|
|
8642
|
+
for (const condData of data.data.rowConditionals) {
|
|
8643
|
+
table._rowConditionals.set(condData.id, {
|
|
8644
|
+
id: condData.id,
|
|
8645
|
+
predicate: condData.predicate,
|
|
8646
|
+
startRowIndex: condData.startRowIndex,
|
|
8647
|
+
endRowIndex: condData.endRowIndex
|
|
8648
|
+
});
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
8063
8651
|
table.updateCoveredCells();
|
|
8064
8652
|
return table;
|
|
8065
8653
|
}
|
|
@@ -8089,6 +8677,18 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8089
8677
|
});
|
|
8090
8678
|
}
|
|
8091
8679
|
}
|
|
8680
|
+
// Restore row conditionals if any
|
|
8681
|
+
this._rowConditionals.clear();
|
|
8682
|
+
if (data.data.rowConditionals) {
|
|
8683
|
+
for (const condData of data.data.rowConditionals) {
|
|
8684
|
+
this._rowConditionals.set(condData.id, {
|
|
8685
|
+
id: condData.id,
|
|
8686
|
+
predicate: condData.predicate,
|
|
8687
|
+
startRowIndex: condData.startRowIndex,
|
|
8688
|
+
endRowIndex: condData.endRowIndex
|
|
8689
|
+
});
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8092
8692
|
// Restore defaults
|
|
8093
8693
|
if (data.data.defaultCellPadding !== undefined) {
|
|
8094
8694
|
this._defaultCellPadding = data.data.defaultCellPadding;
|
|
@@ -8108,6 +8708,13 @@ class TableObject extends BaseEmbeddedObject {
|
|
|
8108
8708
|
return TableObject.fromData(this.toData());
|
|
8109
8709
|
}
|
|
8110
8710
|
}
|
|
8711
|
+
/**
|
|
8712
|
+
* Render row loop indicators (colored stripe on left side of loop rows).
|
|
8713
|
+
*/
|
|
8714
|
+
TableObject.LOOP_COLOR = '#6B46C1';
|
|
8715
|
+
TableObject.LOOP_LABEL_PADDING = 4;
|
|
8716
|
+
TableObject.LOOP_LABEL_RADIUS = 4;
|
|
8717
|
+
TableObject.COND_COLOR = '#D97706'; // Orange
|
|
8111
8718
|
|
|
8112
8719
|
/**
|
|
8113
8720
|
* TableResizeHandler - Handles column and row resize operations for tables.
|
|
@@ -8493,6 +9100,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8493
9100
|
this.substitutionFields = new SubstitutionFieldManager();
|
|
8494
9101
|
this.embeddedObjects = new EmbeddedObjectManager();
|
|
8495
9102
|
this.repeatingSections = new RepeatingSectionManager();
|
|
9103
|
+
this.conditionalSections = new ConditionalSectionManager();
|
|
8496
9104
|
this.hyperlinks = new HyperlinkManager();
|
|
8497
9105
|
this.layout = new TextLayout();
|
|
8498
9106
|
this.setupEventForwarding();
|
|
@@ -8530,6 +9138,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8530
9138
|
this.substitutionFields.handleDeletion(data.start, data.length);
|
|
8531
9139
|
this.embeddedObjects.handleDeletion(data.start, data.length);
|
|
8532
9140
|
this.repeatingSections.handleDeletion(data.start, data.length);
|
|
9141
|
+
this.conditionalSections.handleDeletion(data.start, data.length);
|
|
8533
9142
|
this.paragraphFormatting.handleDeletion(data.start, data.length);
|
|
8534
9143
|
this.hyperlinks.handleDeletion(data.start, data.length);
|
|
8535
9144
|
this.emit('content-changed', {
|
|
@@ -8581,6 +9190,16 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8581
9190
|
this.repeatingSections.on('section-updated', (data) => {
|
|
8582
9191
|
this.emit('repeating-section-updated', data);
|
|
8583
9192
|
});
|
|
9193
|
+
// Forward conditional section events
|
|
9194
|
+
this.conditionalSections.on('section-added', (data) => {
|
|
9195
|
+
this.emit('conditional-section-added', data);
|
|
9196
|
+
});
|
|
9197
|
+
this.conditionalSections.on('section-removed', (data) => {
|
|
9198
|
+
this.emit('conditional-section-removed', data);
|
|
9199
|
+
});
|
|
9200
|
+
this.conditionalSections.on('section-updated', (data) => {
|
|
9201
|
+
this.emit('conditional-section-updated', data);
|
|
9202
|
+
});
|
|
8584
9203
|
// Forward hyperlink events
|
|
8585
9204
|
this.hyperlinks.on('hyperlink-added', (data) => {
|
|
8586
9205
|
this.emit('hyperlink-added', data);
|
|
@@ -8638,6 +9257,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8638
9257
|
this.substitutionFields.shiftFields(insertAt, text.length);
|
|
8639
9258
|
this.embeddedObjects.shiftObjects(insertAt, text.length);
|
|
8640
9259
|
this.repeatingSections.shiftSections(insertAt, text.length);
|
|
9260
|
+
this.conditionalSections.shiftSections(insertAt, text.length);
|
|
8641
9261
|
this.hyperlinks.shiftHyperlinks(insertAt, text.length);
|
|
8642
9262
|
// Insert the text first so we have the full content
|
|
8643
9263
|
this.textState.insertText(text, insertAt);
|
|
@@ -8710,6 +9330,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8710
9330
|
this.substitutionFields.shiftFields(position, text.length);
|
|
8711
9331
|
this.embeddedObjects.shiftObjects(position, text.length);
|
|
8712
9332
|
this.repeatingSections.shiftSections(position, text.length);
|
|
9333
|
+
this.conditionalSections.shiftSections(position, text.length);
|
|
8713
9334
|
this.hyperlinks.shiftHyperlinks(position, text.length);
|
|
8714
9335
|
// Insert the text
|
|
8715
9336
|
const content = this.textState.getText();
|
|
@@ -8727,6 +9348,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
8727
9348
|
this.substitutionFields.handleDeletion(position, length);
|
|
8728
9349
|
this.embeddedObjects.handleDeletion(position, length);
|
|
8729
9350
|
this.repeatingSections.handleDeletion(position, length);
|
|
9351
|
+
this.conditionalSections.handleDeletion(position, length);
|
|
8730
9352
|
this.paragraphFormatting.handleDeletion(position, length);
|
|
8731
9353
|
this.hyperlinks.handleDeletion(position, length);
|
|
8732
9354
|
// Delete the text
|
|
@@ -9116,6 +9738,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9116
9738
|
this.substitutionFields.shiftFields(insertAt, 1);
|
|
9117
9739
|
this.embeddedObjects.shiftObjects(insertAt, 1);
|
|
9118
9740
|
this.repeatingSections.shiftSections(insertAt, 1);
|
|
9741
|
+
this.conditionalSections.shiftSections(insertAt, 1);
|
|
9119
9742
|
// Insert the placeholder character
|
|
9120
9743
|
this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
|
|
9121
9744
|
// Shift paragraph formatting with the complete content
|
|
@@ -9363,6 +9986,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9363
9986
|
this.substitutionFields.clear();
|
|
9364
9987
|
this.embeddedObjects.clear();
|
|
9365
9988
|
this.repeatingSections.clear();
|
|
9989
|
+
this.conditionalSections.clear();
|
|
9366
9990
|
this.hyperlinks.clear();
|
|
9367
9991
|
}
|
|
9368
9992
|
// ============================================
|
|
@@ -9719,44 +10343,60 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9719
10343
|
// List Operations
|
|
9720
10344
|
// ============================================
|
|
9721
10345
|
/**
|
|
9722
|
-
*
|
|
10346
|
+
* Get paragraph starts affected by the current selection or cursor position.
|
|
9723
10347
|
*/
|
|
9724
|
-
|
|
10348
|
+
getAffectedParagraphStarts() {
|
|
10349
|
+
const content = this.textState.getText();
|
|
10350
|
+
const selection = this.getSelection();
|
|
10351
|
+
if (selection && selection.start !== selection.end) {
|
|
10352
|
+
return this.paragraphFormatting.getParagraphBoundariesInRange(selection.start, selection.end, content);
|
|
10353
|
+
}
|
|
9725
10354
|
const cursorPos = this.textState.getCursorPosition();
|
|
10355
|
+
return [this.paragraphFormatting.getParagraphStart(cursorPos, content)];
|
|
10356
|
+
}
|
|
10357
|
+
/**
|
|
10358
|
+
* Toggle bullet list for the current paragraph(s) in selection.
|
|
10359
|
+
*/
|
|
10360
|
+
toggleBulletList() {
|
|
9726
10361
|
const content = this.textState.getText();
|
|
9727
|
-
const
|
|
9728
|
-
|
|
9729
|
-
|
|
10362
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10363
|
+
for (const start of paragraphStarts) {
|
|
10364
|
+
this.paragraphFormatting.toggleList(start, 'bullet');
|
|
10365
|
+
}
|
|
10366
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9730
10367
|
}
|
|
9731
10368
|
/**
|
|
9732
|
-
* Toggle numbered list for the current paragraph
|
|
10369
|
+
* Toggle numbered list for the current paragraph(s) in selection.
|
|
9733
10370
|
*/
|
|
9734
10371
|
toggleNumberedList() {
|
|
9735
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9736
10372
|
const content = this.textState.getText();
|
|
9737
|
-
const
|
|
9738
|
-
|
|
9739
|
-
|
|
10373
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10374
|
+
for (const start of paragraphStarts) {
|
|
10375
|
+
this.paragraphFormatting.toggleList(start, 'number');
|
|
10376
|
+
}
|
|
10377
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9740
10378
|
}
|
|
9741
10379
|
/**
|
|
9742
|
-
* Indent the current paragraph
|
|
10380
|
+
* Indent the current paragraph(s) in selection.
|
|
9743
10381
|
*/
|
|
9744
10382
|
indentParagraph() {
|
|
9745
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9746
10383
|
const content = this.textState.getText();
|
|
9747
|
-
const
|
|
9748
|
-
|
|
9749
|
-
|
|
10384
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10385
|
+
for (const start of paragraphStarts) {
|
|
10386
|
+
this.paragraphFormatting.indentParagraph(start);
|
|
10387
|
+
}
|
|
10388
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9750
10389
|
}
|
|
9751
10390
|
/**
|
|
9752
|
-
* Outdent the current paragraph
|
|
10391
|
+
* Outdent the current paragraph(s) in selection.
|
|
9753
10392
|
*/
|
|
9754
10393
|
outdentParagraph() {
|
|
9755
|
-
const cursorPos = this.textState.getCursorPosition();
|
|
9756
10394
|
const content = this.textState.getText();
|
|
9757
|
-
const
|
|
9758
|
-
|
|
9759
|
-
|
|
10395
|
+
const paragraphStarts = this.getAffectedParagraphStarts();
|
|
10396
|
+
for (const start of paragraphStarts) {
|
|
10397
|
+
this.paragraphFormatting.outdentParagraph(start);
|
|
10398
|
+
}
|
|
10399
|
+
this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
|
|
9760
10400
|
}
|
|
9761
10401
|
/**
|
|
9762
10402
|
* Get the list formatting for the current paragraph.
|
|
@@ -9918,6 +10558,79 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9918
10558
|
return result;
|
|
9919
10559
|
}
|
|
9920
10560
|
// ============================================
|
|
10561
|
+
// Conditional Section Operations
|
|
10562
|
+
// ============================================
|
|
10563
|
+
/**
|
|
10564
|
+
* Get the conditional section manager.
|
|
10565
|
+
*/
|
|
10566
|
+
getConditionalSectionManager() {
|
|
10567
|
+
return this.conditionalSections;
|
|
10568
|
+
}
|
|
10569
|
+
/**
|
|
10570
|
+
* Get all conditional sections.
|
|
10571
|
+
*/
|
|
10572
|
+
getConditionalSections() {
|
|
10573
|
+
return this.conditionalSections.getSections();
|
|
10574
|
+
}
|
|
10575
|
+
/**
|
|
10576
|
+
* Create a conditional section.
|
|
10577
|
+
* @param startIndex Text index at paragraph start (must be at a paragraph boundary)
|
|
10578
|
+
* @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
|
|
10579
|
+
* @param predicate The predicate expression to evaluate
|
|
10580
|
+
* @returns The created section, or null if boundaries are invalid
|
|
10581
|
+
*/
|
|
10582
|
+
createConditionalSection(startIndex, endIndex, predicate) {
|
|
10583
|
+
const content = this.textState.getText();
|
|
10584
|
+
if (!this.conditionalSections.validateBoundaries(startIndex, endIndex, content)) {
|
|
10585
|
+
return null;
|
|
10586
|
+
}
|
|
10587
|
+
const section = this.conditionalSections.create(startIndex, endIndex, predicate);
|
|
10588
|
+
this.emit('content-changed', {
|
|
10589
|
+
text: content,
|
|
10590
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10591
|
+
});
|
|
10592
|
+
return section;
|
|
10593
|
+
}
|
|
10594
|
+
/**
|
|
10595
|
+
* Remove a conditional section by ID.
|
|
10596
|
+
*/
|
|
10597
|
+
removeConditionalSection(id) {
|
|
10598
|
+
const section = this.conditionalSections.remove(id);
|
|
10599
|
+
if (section) {
|
|
10600
|
+
this.emit('content-changed', {
|
|
10601
|
+
text: this.textState.getText(),
|
|
10602
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10603
|
+
});
|
|
10604
|
+
return true;
|
|
10605
|
+
}
|
|
10606
|
+
return false;
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* Get a conditional section by ID.
|
|
10610
|
+
*/
|
|
10611
|
+
getConditionalSection(id) {
|
|
10612
|
+
return this.conditionalSections.getSection(id);
|
|
10613
|
+
}
|
|
10614
|
+
/**
|
|
10615
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
10616
|
+
*/
|
|
10617
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
10618
|
+
return this.conditionalSections.getSectionAtBoundary(textIndex);
|
|
10619
|
+
}
|
|
10620
|
+
/**
|
|
10621
|
+
* Update a conditional section's predicate.
|
|
10622
|
+
*/
|
|
10623
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
10624
|
+
const result = this.conditionalSections.updatePredicate(id, predicate);
|
|
10625
|
+
if (result) {
|
|
10626
|
+
this.emit('content-changed', {
|
|
10627
|
+
text: this.textState.getText(),
|
|
10628
|
+
cursorPosition: this.textState.getCursorPosition()
|
|
10629
|
+
});
|
|
10630
|
+
}
|
|
10631
|
+
return result;
|
|
10632
|
+
}
|
|
10633
|
+
// ============================================
|
|
9921
10634
|
// Serialization
|
|
9922
10635
|
// ============================================
|
|
9923
10636
|
/**
|
|
@@ -9971,6 +10684,8 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9971
10684
|
}));
|
|
9972
10685
|
// Serialize repeating sections
|
|
9973
10686
|
const repeatingSectionsData = this.repeatingSections.toJSON();
|
|
10687
|
+
// Serialize conditional sections
|
|
10688
|
+
const conditionalSectionsData = this.conditionalSections.toJSON();
|
|
9974
10689
|
// Serialize embedded objects
|
|
9975
10690
|
const embeddedObjects = [];
|
|
9976
10691
|
const objectsMap = this.embeddedObjects.getObjects();
|
|
@@ -9988,6 +10703,7 @@ class FlowingTextContent extends EventEmitter {
|
|
|
9988
10703
|
paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
|
|
9989
10704
|
substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
|
|
9990
10705
|
repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
|
|
10706
|
+
conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
|
|
9991
10707
|
embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
|
|
9992
10708
|
hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
|
|
9993
10709
|
};
|
|
@@ -10026,6 +10742,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10026
10742
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10027
10743
|
content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
|
|
10028
10744
|
}
|
|
10745
|
+
// Restore conditional sections
|
|
10746
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10747
|
+
content.getConditionalSectionManager().fromJSON(data.conditionalSections);
|
|
10748
|
+
}
|
|
10029
10749
|
// Restore embedded objects using factory
|
|
10030
10750
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10031
10751
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10080,6 +10800,10 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10080
10800
|
if (data.repeatingSections && data.repeatingSections.length > 0) {
|
|
10081
10801
|
this.repeatingSections.fromJSON(data.repeatingSections);
|
|
10082
10802
|
}
|
|
10803
|
+
// Restore conditional sections
|
|
10804
|
+
if (data.conditionalSections && data.conditionalSections.length > 0) {
|
|
10805
|
+
this.conditionalSections.fromJSON(data.conditionalSections);
|
|
10806
|
+
}
|
|
10083
10807
|
// Restore embedded objects
|
|
10084
10808
|
if (data.embeddedObjects && data.embeddedObjects.length > 0) {
|
|
10085
10809
|
for (const ref of data.embeddedObjects) {
|
|
@@ -10099,6 +10823,349 @@ class FlowingTextContent extends EventEmitter {
|
|
|
10099
10823
|
}
|
|
10100
10824
|
}
|
|
10101
10825
|
|
|
10826
|
+
/**
|
|
10827
|
+
* Simple recursive-descent predicate evaluator.
|
|
10828
|
+
* Supports:
|
|
10829
|
+
* - Truthiness: `isActive`
|
|
10830
|
+
* - Negation: `!isActive`
|
|
10831
|
+
* - Comparisons: ==, !=, >, <, >=, <=
|
|
10832
|
+
* - Logical: &&, ||, parentheses
|
|
10833
|
+
* - Literals: "approved", 100, true/false
|
|
10834
|
+
* - Dot notation: customer.isVIP
|
|
10835
|
+
*/
|
|
10836
|
+
function tokenize(input) {
|
|
10837
|
+
const tokens = [];
|
|
10838
|
+
let i = 0;
|
|
10839
|
+
while (i < input.length) {
|
|
10840
|
+
const ch = input[i];
|
|
10841
|
+
// Skip whitespace
|
|
10842
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
10843
|
+
i++;
|
|
10844
|
+
continue;
|
|
10845
|
+
}
|
|
10846
|
+
// Parentheses
|
|
10847
|
+
if (ch === '(' || ch === ')') {
|
|
10848
|
+
tokens.push({ type: 'paren', value: ch });
|
|
10849
|
+
i++;
|
|
10850
|
+
continue;
|
|
10851
|
+
}
|
|
10852
|
+
// Two-character operators
|
|
10853
|
+
if (i + 1 < input.length) {
|
|
10854
|
+
const two = input[i] + input[i + 1];
|
|
10855
|
+
if (two === '==' || two === '!=' || two === '>=' || two === '<=' || two === '&&' || two === '||' || two === '=~' || two === '!~') {
|
|
10856
|
+
tokens.push({ type: 'operator', value: two });
|
|
10857
|
+
i += 2;
|
|
10858
|
+
continue;
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10861
|
+
// Single-character operators
|
|
10862
|
+
if (ch === '>' || ch === '<') {
|
|
10863
|
+
tokens.push({ type: 'operator', value: ch });
|
|
10864
|
+
i++;
|
|
10865
|
+
continue;
|
|
10866
|
+
}
|
|
10867
|
+
// Not operator
|
|
10868
|
+
if (ch === '!') {
|
|
10869
|
+
tokens.push({ type: 'not' });
|
|
10870
|
+
i++;
|
|
10871
|
+
continue;
|
|
10872
|
+
}
|
|
10873
|
+
// String literals
|
|
10874
|
+
if (ch === '"' || ch === "'") {
|
|
10875
|
+
const quote = ch;
|
|
10876
|
+
i++;
|
|
10877
|
+
let str = '';
|
|
10878
|
+
while (i < input.length && input[i] !== quote) {
|
|
10879
|
+
if (input[i] === '\\' && i + 1 < input.length) {
|
|
10880
|
+
i++;
|
|
10881
|
+
str += input[i];
|
|
10882
|
+
}
|
|
10883
|
+
else {
|
|
10884
|
+
str += input[i];
|
|
10885
|
+
}
|
|
10886
|
+
i++;
|
|
10887
|
+
}
|
|
10888
|
+
i++; // skip closing quote
|
|
10889
|
+
tokens.push({ type: 'string', value: str });
|
|
10890
|
+
continue;
|
|
10891
|
+
}
|
|
10892
|
+
// Numbers
|
|
10893
|
+
if (ch >= '0' && ch <= '9') {
|
|
10894
|
+
let num = '';
|
|
10895
|
+
while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
|
|
10896
|
+
num += input[i];
|
|
10897
|
+
i++;
|
|
10898
|
+
}
|
|
10899
|
+
tokens.push({ type: 'number', value: parseFloat(num) });
|
|
10900
|
+
continue;
|
|
10901
|
+
}
|
|
10902
|
+
// Identifiers (including dot notation: customer.isVIP)
|
|
10903
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
10904
|
+
let ident = '';
|
|
10905
|
+
while (i < input.length &&
|
|
10906
|
+
((input[i] >= 'a' && input[i] <= 'z') ||
|
|
10907
|
+
(input[i] >= 'A' && input[i] <= 'Z') ||
|
|
10908
|
+
(input[i] >= '0' && input[i] <= '9') ||
|
|
10909
|
+
input[i] === '_' || input[i] === '$' || input[i] === '.')) {
|
|
10910
|
+
ident += input[i];
|
|
10911
|
+
i++;
|
|
10912
|
+
}
|
|
10913
|
+
if (ident === 'true') {
|
|
10914
|
+
tokens.push({ type: 'boolean', value: true });
|
|
10915
|
+
}
|
|
10916
|
+
else if (ident === 'false') {
|
|
10917
|
+
tokens.push({ type: 'boolean', value: false });
|
|
10918
|
+
}
|
|
10919
|
+
else {
|
|
10920
|
+
tokens.push({ type: 'identifier', value: ident });
|
|
10921
|
+
}
|
|
10922
|
+
continue;
|
|
10923
|
+
}
|
|
10924
|
+
// Unknown character — skip
|
|
10925
|
+
i++;
|
|
10926
|
+
}
|
|
10927
|
+
tokens.push({ type: 'eof' });
|
|
10928
|
+
return tokens;
|
|
10929
|
+
}
|
|
10930
|
+
class Parser {
|
|
10931
|
+
constructor(tokens, data) {
|
|
10932
|
+
this.pos = 0;
|
|
10933
|
+
this.tokens = tokens;
|
|
10934
|
+
this.data = data;
|
|
10935
|
+
}
|
|
10936
|
+
peek() {
|
|
10937
|
+
return this.tokens[this.pos];
|
|
10938
|
+
}
|
|
10939
|
+
advance() {
|
|
10940
|
+
const token = this.tokens[this.pos];
|
|
10941
|
+
this.pos++;
|
|
10942
|
+
return token;
|
|
10943
|
+
}
|
|
10944
|
+
/**
|
|
10945
|
+
* Parse the full expression.
|
|
10946
|
+
* Grammar:
|
|
10947
|
+
* expr → or_expr
|
|
10948
|
+
* or_expr → and_expr ('||' and_expr)*
|
|
10949
|
+
* and_expr → unary (('==' | '!=' | '>' | '<' | '>=' | '<=') unary)?
|
|
10950
|
+
* | unary ('&&' unary_or_comparison)*
|
|
10951
|
+
* unary → '!' unary | primary
|
|
10952
|
+
* primary → '(' expr ')' | literal | identifier
|
|
10953
|
+
*/
|
|
10954
|
+
parse() {
|
|
10955
|
+
const result = this.parseOr();
|
|
10956
|
+
return result;
|
|
10957
|
+
}
|
|
10958
|
+
parseOr() {
|
|
10959
|
+
let left = this.parseAnd();
|
|
10960
|
+
while (this.peek().type === 'operator' && this.peek().value === '||') {
|
|
10961
|
+
this.advance();
|
|
10962
|
+
const right = this.parseAnd();
|
|
10963
|
+
left = this.isTruthy(left) || this.isTruthy(right);
|
|
10964
|
+
}
|
|
10965
|
+
return left;
|
|
10966
|
+
}
|
|
10967
|
+
parseAnd() {
|
|
10968
|
+
let left = this.parseComparison();
|
|
10969
|
+
while (this.peek().type === 'operator' && this.peek().value === '&&') {
|
|
10970
|
+
this.advance();
|
|
10971
|
+
const right = this.parseComparison();
|
|
10972
|
+
left = this.isTruthy(left) && this.isTruthy(right);
|
|
10973
|
+
}
|
|
10974
|
+
return left;
|
|
10975
|
+
}
|
|
10976
|
+
parseComparison() {
|
|
10977
|
+
const left = this.parseUnary();
|
|
10978
|
+
const token = this.peek();
|
|
10979
|
+
if (token.type === 'operator') {
|
|
10980
|
+
const op = token.value;
|
|
10981
|
+
if (op === '==' || op === '!=' || op === '>' || op === '<' || op === '>=' || op === '<=' || op === '=~' || op === '!~') {
|
|
10982
|
+
this.advance();
|
|
10983
|
+
const right = this.parseUnary();
|
|
10984
|
+
return this.compare(left, op, right);
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
return left;
|
|
10988
|
+
}
|
|
10989
|
+
parseUnary() {
|
|
10990
|
+
if (this.peek().type === 'not') {
|
|
10991
|
+
this.advance();
|
|
10992
|
+
const value = this.parseUnary();
|
|
10993
|
+
return !this.isTruthy(value);
|
|
10994
|
+
}
|
|
10995
|
+
return this.parsePrimary();
|
|
10996
|
+
}
|
|
10997
|
+
parsePrimary() {
|
|
10998
|
+
const token = this.peek();
|
|
10999
|
+
if (token.type === 'paren' && token.value === '(') {
|
|
11000
|
+
this.advance();
|
|
11001
|
+
const value = this.parseOr();
|
|
11002
|
+
// Consume closing paren
|
|
11003
|
+
if (this.peek().type === 'paren' && this.peek().value === ')') {
|
|
11004
|
+
this.advance();
|
|
11005
|
+
}
|
|
11006
|
+
return value;
|
|
11007
|
+
}
|
|
11008
|
+
if (token.type === 'string') {
|
|
11009
|
+
this.advance();
|
|
11010
|
+
return token.value;
|
|
11011
|
+
}
|
|
11012
|
+
if (token.type === 'number') {
|
|
11013
|
+
this.advance();
|
|
11014
|
+
return token.value;
|
|
11015
|
+
}
|
|
11016
|
+
if (token.type === 'boolean') {
|
|
11017
|
+
this.advance();
|
|
11018
|
+
return token.value;
|
|
11019
|
+
}
|
|
11020
|
+
if (token.type === 'identifier') {
|
|
11021
|
+
this.advance();
|
|
11022
|
+
return this.resolveIdentifier(token.value);
|
|
11023
|
+
}
|
|
11024
|
+
// EOF or unexpected — return undefined
|
|
11025
|
+
this.advance();
|
|
11026
|
+
return undefined;
|
|
11027
|
+
}
|
|
11028
|
+
resolveIdentifier(path) {
|
|
11029
|
+
const parts = path.split('.');
|
|
11030
|
+
let current = this.data;
|
|
11031
|
+
for (const part of parts) {
|
|
11032
|
+
if (current === null || current === undefined) {
|
|
11033
|
+
return undefined;
|
|
11034
|
+
}
|
|
11035
|
+
if (typeof current === 'object') {
|
|
11036
|
+
current = current[part];
|
|
11037
|
+
}
|
|
11038
|
+
else {
|
|
11039
|
+
return undefined;
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
return current;
|
|
11043
|
+
}
|
|
11044
|
+
compare(left, op, right) {
|
|
11045
|
+
// Regex match: left is coerced to string, right is the pattern string
|
|
11046
|
+
if (op === '=~' || op === '!~') {
|
|
11047
|
+
const str = this.toString(left);
|
|
11048
|
+
const pattern = this.toString(right);
|
|
11049
|
+
try {
|
|
11050
|
+
const regex = new RegExp(pattern);
|
|
11051
|
+
const matches = regex.test(str);
|
|
11052
|
+
return op === '=~' ? matches : !matches;
|
|
11053
|
+
}
|
|
11054
|
+
catch {
|
|
11055
|
+
// Invalid regex pattern — treat as no match
|
|
11056
|
+
return op === '!~';
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
// For ordering operators, coerce both sides to numbers if either side is numeric
|
|
11060
|
+
if (op === '>' || op === '<' || op === '>=' || op === '<=') {
|
|
11061
|
+
const l = this.toNumber(left);
|
|
11062
|
+
const r = this.toNumber(right);
|
|
11063
|
+
switch (op) {
|
|
11064
|
+
case '>': return l > r;
|
|
11065
|
+
case '<': return l < r;
|
|
11066
|
+
case '>=': return l >= r;
|
|
11067
|
+
case '<=': return l <= r;
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
// For equality, coerce to numbers if both sides look numeric
|
|
11071
|
+
const ln = this.toNumberIfNumeric(left);
|
|
11072
|
+
const rn = this.toNumberIfNumeric(right);
|
|
11073
|
+
switch (op) {
|
|
11074
|
+
case '==': return ln == rn; // eslint-disable-line eqeqeq
|
|
11075
|
+
case '!=': return ln != rn; // eslint-disable-line eqeqeq
|
|
11076
|
+
default: return false;
|
|
11077
|
+
}
|
|
11078
|
+
}
|
|
11079
|
+
/**
|
|
11080
|
+
* Convert a value to a string for regex matching.
|
|
11081
|
+
*/
|
|
11082
|
+
toString(value) {
|
|
11083
|
+
if (value === null || value === undefined)
|
|
11084
|
+
return '';
|
|
11085
|
+
if (typeof value === 'string')
|
|
11086
|
+
return value;
|
|
11087
|
+
return String(value);
|
|
11088
|
+
}
|
|
11089
|
+
/**
|
|
11090
|
+
* Convert a value to a number. Strings that look like numbers are parsed.
|
|
11091
|
+
* Non-numeric values become NaN.
|
|
11092
|
+
*/
|
|
11093
|
+
toNumber(value) {
|
|
11094
|
+
if (typeof value === 'number')
|
|
11095
|
+
return value;
|
|
11096
|
+
if (typeof value === 'string') {
|
|
11097
|
+
const n = Number(value);
|
|
11098
|
+
return isNaN(n) ? NaN : n;
|
|
11099
|
+
}
|
|
11100
|
+
if (typeof value === 'boolean')
|
|
11101
|
+
return value ? 1 : 0;
|
|
11102
|
+
return NaN;
|
|
11103
|
+
}
|
|
11104
|
+
/**
|
|
11105
|
+
* If a value is a string that looks like a number, convert it.
|
|
11106
|
+
* Otherwise return the value as-is. Used for == / != so that
|
|
11107
|
+
* "5" == 5 is true but "hello" == "hello" still works.
|
|
11108
|
+
*/
|
|
11109
|
+
toNumberIfNumeric(value) {
|
|
11110
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
11111
|
+
const n = Number(value);
|
|
11112
|
+
if (!isNaN(n))
|
|
11113
|
+
return n;
|
|
11114
|
+
}
|
|
11115
|
+
return value;
|
|
11116
|
+
}
|
|
11117
|
+
isTruthy(value) {
|
|
11118
|
+
if (value === null || value === undefined)
|
|
11119
|
+
return false;
|
|
11120
|
+
if (typeof value === 'boolean')
|
|
11121
|
+
return value;
|
|
11122
|
+
if (typeof value === 'number')
|
|
11123
|
+
return value !== 0;
|
|
11124
|
+
if (typeof value === 'string')
|
|
11125
|
+
return value.length > 0;
|
|
11126
|
+
if (Array.isArray(value))
|
|
11127
|
+
return value.length > 0;
|
|
11128
|
+
return true;
|
|
11129
|
+
}
|
|
11130
|
+
}
|
|
11131
|
+
/**
|
|
11132
|
+
* Static predicate evaluator for conditional sections.
|
|
11133
|
+
*/
|
|
11134
|
+
class PredicateEvaluator {
|
|
11135
|
+
/**
|
|
11136
|
+
* Evaluate a predicate expression against data.
|
|
11137
|
+
* @param predicate The predicate string (e.g., "isActive", "count > 0")
|
|
11138
|
+
* @param data The data context to evaluate against
|
|
11139
|
+
* @returns true if the predicate is truthy, false otherwise
|
|
11140
|
+
*/
|
|
11141
|
+
static evaluate(predicate, data) {
|
|
11142
|
+
if (!predicate || predicate.trim().length === 0) {
|
|
11143
|
+
return false;
|
|
11144
|
+
}
|
|
11145
|
+
try {
|
|
11146
|
+
const tokens = tokenize(predicate.trim());
|
|
11147
|
+
const parser = new Parser(tokens, data);
|
|
11148
|
+
const result = parser.parse();
|
|
11149
|
+
// Convert result to boolean
|
|
11150
|
+
if (result === null || result === undefined)
|
|
11151
|
+
return false;
|
|
11152
|
+
if (typeof result === 'boolean')
|
|
11153
|
+
return result;
|
|
11154
|
+
if (typeof result === 'number')
|
|
11155
|
+
return result !== 0;
|
|
11156
|
+
if (typeof result === 'string')
|
|
11157
|
+
return result.length > 0;
|
|
11158
|
+
if (Array.isArray(result))
|
|
11159
|
+
return result.length > 0;
|
|
11160
|
+
return true;
|
|
11161
|
+
}
|
|
11162
|
+
catch {
|
|
11163
|
+
// If evaluation fails, treat as false
|
|
11164
|
+
return false;
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
|
|
10102
11169
|
/**
|
|
10103
11170
|
* Abstract base class providing common functionality for regions.
|
|
10104
11171
|
*/
|
|
@@ -11134,6 +12201,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
|
|
|
11134
12201
|
const LOOP_LABEL_PADDING = 4;
|
|
11135
12202
|
const LOOP_LABEL_RADIUS = 4;
|
|
11136
12203
|
const LOOP_LINE_DASH = [4, 4];
|
|
12204
|
+
// Conditional section indicator styling
|
|
12205
|
+
const COND_INDICATOR_COLOR = '#D97706'; // Orange
|
|
12206
|
+
const COND_LABEL_PADDING = 4;
|
|
12207
|
+
const COND_LABEL_RADIUS = 4;
|
|
12208
|
+
const COND_LINE_DASH = [4, 4];
|
|
11137
12209
|
// Hyperlink styling
|
|
11138
12210
|
const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
|
|
11139
12211
|
class FlowingTextRenderer extends EventEmitter {
|
|
@@ -11391,8 +12463,6 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11391
12463
|
if (pageIndex === 0) {
|
|
11392
12464
|
// Clear table continuations when starting a new render cycle
|
|
11393
12465
|
this.clearTableContinuations();
|
|
11394
|
-
// Clear content hit targets - they will be re-registered during render
|
|
11395
|
-
this._hitTestManager.clearCategory('content');
|
|
11396
12466
|
// This is the first page, flow all text
|
|
11397
12467
|
const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
|
|
11398
12468
|
this.flowedPages.set(page.id, flowedPages);
|
|
@@ -11692,6 +12762,8 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11692
12762
|
const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
|
|
11693
12763
|
// Get hyperlinks for rendering
|
|
11694
12764
|
const hyperlinks = flowingContent.getAllHyperlinks();
|
|
12765
|
+
// Track relative objects to render after all lines (so they appear on top)
|
|
12766
|
+
const relativeObjects = [];
|
|
11695
12767
|
// Render each line
|
|
11696
12768
|
let y = bounds.y;
|
|
11697
12769
|
for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
|
|
@@ -11704,6 +12776,18 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11704
12776
|
if (clipToBounds && y > bounds.y + bounds.height) {
|
|
11705
12777
|
break;
|
|
11706
12778
|
}
|
|
12779
|
+
// Collect relative objects from this line
|
|
12780
|
+
if (line.embeddedObjects) {
|
|
12781
|
+
for (const embeddedObj of line.embeddedObjects) {
|
|
12782
|
+
if (embeddedObj.isAnchor && embeddedObj.object.position === 'relative') {
|
|
12783
|
+
relativeObjects.push({
|
|
12784
|
+
object: embeddedObj.object,
|
|
12785
|
+
anchorX: bounds.x,
|
|
12786
|
+
anchorY: y
|
|
12787
|
+
});
|
|
12788
|
+
}
|
|
12789
|
+
}
|
|
12790
|
+
}
|
|
11707
12791
|
this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
|
|
11708
12792
|
y += line.height;
|
|
11709
12793
|
}
|
|
@@ -11714,6 +12798,10 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
11714
12798
|
if (clipToBounds) {
|
|
11715
12799
|
ctx.restore();
|
|
11716
12800
|
}
|
|
12801
|
+
// Render relative objects on top of text (outside clip region)
|
|
12802
|
+
if (relativeObjects.length > 0) {
|
|
12803
|
+
this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
|
|
12804
|
+
}
|
|
11717
12805
|
}
|
|
11718
12806
|
/**
|
|
11719
12807
|
* Render selection highlight for a region.
|
|
@@ -13773,11 +14861,283 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13773
14861
|
verticalEndY = endInfo.y;
|
|
13774
14862
|
}
|
|
13775
14863
|
else if (endsAfterPage) {
|
|
13776
|
-
// Section continues to next page, end at bottom of content area
|
|
14864
|
+
// Section continues to next page, end at bottom of content area
|
|
14865
|
+
verticalEndY = contentBounds.y + contentBounds.height;
|
|
14866
|
+
}
|
|
14867
|
+
else {
|
|
14868
|
+
verticalEndY = verticalStartY; // No vertical line if neither start nor end
|
|
14869
|
+
}
|
|
14870
|
+
if (verticalEndY > verticalStartY) {
|
|
14871
|
+
ctx.beginPath();
|
|
14872
|
+
ctx.moveTo(connectorX, verticalStartY);
|
|
14873
|
+
ctx.lineTo(connectorX, verticalEndY);
|
|
14874
|
+
ctx.stroke();
|
|
14875
|
+
}
|
|
14876
|
+
// Draw "Loop" label last so it's in front of all lines
|
|
14877
|
+
if (hasStart) {
|
|
14878
|
+
const startY = startInfo.y;
|
|
14879
|
+
this.drawLoopLabel(ctx, labelX, startY - 10, 'Loop', isSelected);
|
|
14880
|
+
}
|
|
14881
|
+
// Update section's visual state
|
|
14882
|
+
section.visualState = {
|
|
14883
|
+
startPageIndex: hasStart ? pageIndex : -1,
|
|
14884
|
+
startY: hasStart ? startInfo.y : 0,
|
|
14885
|
+
endPageIndex: hasEnd ? pageIndex : -1,
|
|
14886
|
+
endY: hasEnd ? endInfo.y : 0,
|
|
14887
|
+
spansMultiplePages: !hasStart || !hasEnd
|
|
14888
|
+
};
|
|
14889
|
+
ctx.restore();
|
|
14890
|
+
}
|
|
14891
|
+
/**
|
|
14892
|
+
* Draw the "Loop" label in a rounded rectangle.
|
|
14893
|
+
* When not selected, draws an outlined rectangle.
|
|
14894
|
+
* When selected, draws a filled rectangle.
|
|
14895
|
+
*/
|
|
14896
|
+
drawLoopLabel(ctx, x, y, text, isSelected = false) {
|
|
14897
|
+
ctx.save();
|
|
14898
|
+
ctx.font = '10px Arial';
|
|
14899
|
+
const metrics = ctx.measureText(text);
|
|
14900
|
+
const textWidth = metrics.width;
|
|
14901
|
+
const textHeight = 10;
|
|
14902
|
+
const boxWidth = textWidth + LOOP_LABEL_PADDING * 2;
|
|
14903
|
+
const boxHeight = textHeight + LOOP_LABEL_PADDING * 2;
|
|
14904
|
+
ctx.beginPath();
|
|
14905
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, LOOP_LABEL_RADIUS);
|
|
14906
|
+
if (isSelected) {
|
|
14907
|
+
// Selected: filled background with white text
|
|
14908
|
+
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
14909
|
+
ctx.fill();
|
|
14910
|
+
ctx.fillStyle = '#ffffff';
|
|
14911
|
+
}
|
|
14912
|
+
else {
|
|
14913
|
+
// Not selected: white background, outlined with colored text
|
|
14914
|
+
ctx.fillStyle = '#ffffff';
|
|
14915
|
+
ctx.fill();
|
|
14916
|
+
ctx.strokeStyle = LOOP_INDICATOR_COLOR;
|
|
14917
|
+
ctx.lineWidth = 1.5;
|
|
14918
|
+
ctx.stroke();
|
|
14919
|
+
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
14920
|
+
}
|
|
14921
|
+
// Draw text
|
|
14922
|
+
ctx.textBaseline = 'middle';
|
|
14923
|
+
ctx.fillText(text, x + LOOP_LABEL_PADDING, y + boxHeight / 2);
|
|
14924
|
+
ctx.restore();
|
|
14925
|
+
}
|
|
14926
|
+
/**
|
|
14927
|
+
* Draw a rounded rectangle path.
|
|
14928
|
+
*/
|
|
14929
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
14930
|
+
ctx.moveTo(x + radius, y);
|
|
14931
|
+
ctx.lineTo(x + width - radius, y);
|
|
14932
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
14933
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
14934
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
14935
|
+
ctx.lineTo(x + radius, y + height);
|
|
14936
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
14937
|
+
ctx.lineTo(x, y + radius);
|
|
14938
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
14939
|
+
ctx.closePath();
|
|
14940
|
+
}
|
|
14941
|
+
/**
|
|
14942
|
+
* Find the Y position for a text index on a flowed page.
|
|
14943
|
+
* Returns the Y position at the TOP of the line containing the text index.
|
|
14944
|
+
*/
|
|
14945
|
+
findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
|
|
14946
|
+
let y = contentBounds.y;
|
|
14947
|
+
for (let i = 0; i < flowedPage.lines.length; i++) {
|
|
14948
|
+
const line = flowedPage.lines[i];
|
|
14949
|
+
// Check if this line contains the text index
|
|
14950
|
+
if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
|
|
14951
|
+
return { y, lineIndex: i };
|
|
14952
|
+
}
|
|
14953
|
+
// Check if text index is exactly at the start of this line
|
|
14954
|
+
// (for section boundaries that are at paragraph starts)
|
|
14955
|
+
if (textIndex === line.startIndex) {
|
|
14956
|
+
return { y, lineIndex: i };
|
|
14957
|
+
}
|
|
14958
|
+
y += line.height;
|
|
14959
|
+
}
|
|
14960
|
+
// Check if text index is just past the last line (end of content)
|
|
14961
|
+
if (flowedPage.lines.length > 0) {
|
|
14962
|
+
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
14963
|
+
if (textIndex === lastLine.endIndex + 1) {
|
|
14964
|
+
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
14965
|
+
}
|
|
14966
|
+
}
|
|
14967
|
+
return null;
|
|
14968
|
+
}
|
|
14969
|
+
/**
|
|
14970
|
+
* Check if a section spans across a flowed page (starts before and ends after).
|
|
14971
|
+
*/
|
|
14972
|
+
sectionSpansPage(section, flowedPage) {
|
|
14973
|
+
if (flowedPage.lines.length === 0)
|
|
14974
|
+
return false;
|
|
14975
|
+
const pageStart = flowedPage.startIndex;
|
|
14976
|
+
const pageEnd = flowedPage.endIndex;
|
|
14977
|
+
// Section spans this page if it started before and ends after
|
|
14978
|
+
return section.startIndex < pageStart && section.endIndex > pageEnd;
|
|
14979
|
+
}
|
|
14980
|
+
/**
|
|
14981
|
+
* Get a repeating section at a point (for click detection).
|
|
14982
|
+
* Checks if the point is on the Loop label or vertical connector.
|
|
14983
|
+
*/
|
|
14984
|
+
getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
|
|
14985
|
+
const labelX = pageBounds.x + 5;
|
|
14986
|
+
const labelWidth = 32;
|
|
14987
|
+
const connectorX = labelX + labelWidth / 2;
|
|
14988
|
+
const hitRadius = 10; // Pixels for click detection
|
|
14989
|
+
for (const section of sections) {
|
|
14990
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
14991
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
14992
|
+
const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
|
|
14993
|
+
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
14994
|
+
continue;
|
|
14995
|
+
}
|
|
14996
|
+
// Check if click is on the Loop label
|
|
14997
|
+
if (startInfo) {
|
|
14998
|
+
const labelY = startInfo.y - 10;
|
|
14999
|
+
const labelHeight = 18;
|
|
15000
|
+
if (point.x >= labelX &&
|
|
15001
|
+
point.x <= labelX + labelWidth &&
|
|
15002
|
+
point.y >= labelY &&
|
|
15003
|
+
point.y <= labelY + labelHeight) {
|
|
15004
|
+
return section;
|
|
15005
|
+
}
|
|
15006
|
+
}
|
|
15007
|
+
// Check if click is on the vertical connector line
|
|
15008
|
+
let verticalStartY;
|
|
15009
|
+
let verticalEndY;
|
|
15010
|
+
if (startInfo) {
|
|
15011
|
+
verticalStartY = startInfo.y;
|
|
15012
|
+
}
|
|
15013
|
+
else {
|
|
15014
|
+
verticalStartY = contentBounds.y;
|
|
15015
|
+
}
|
|
15016
|
+
if (endInfo) {
|
|
15017
|
+
verticalEndY = endInfo.y;
|
|
15018
|
+
}
|
|
15019
|
+
else if (sectionSpansThisPage) {
|
|
15020
|
+
verticalEndY = contentBounds.y + flowedPage.height;
|
|
15021
|
+
}
|
|
15022
|
+
else {
|
|
15023
|
+
continue;
|
|
15024
|
+
}
|
|
15025
|
+
if (Math.abs(point.x - connectorX) <= hitRadius &&
|
|
15026
|
+
point.y >= verticalStartY &&
|
|
15027
|
+
point.y <= verticalEndY) {
|
|
15028
|
+
return section;
|
|
15029
|
+
}
|
|
15030
|
+
}
|
|
15031
|
+
return null;
|
|
15032
|
+
}
|
|
15033
|
+
// ============================================
|
|
15034
|
+
// Conditional Section Indicators
|
|
15035
|
+
// ============================================
|
|
15036
|
+
/**
|
|
15037
|
+
* Render conditional section indicators for a page.
|
|
15038
|
+
*/
|
|
15039
|
+
renderConditionalSectionIndicators(sections, pageIndex, ctx, contentBounds, flowedPage, pageBounds, selectedSectionId = null) {
|
|
15040
|
+
for (const section of sections) {
|
|
15041
|
+
this.renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, pageBounds, section.id === selectedSectionId);
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
/**
|
|
15045
|
+
* Render a single conditional section indicator.
|
|
15046
|
+
*/
|
|
15047
|
+
renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, _pageBounds, isSelected = false) {
|
|
15048
|
+
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
15049
|
+
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
15050
|
+
const sectionOverlapsPage = section.startIndex < flowedPage.endIndex &&
|
|
15051
|
+
section.endIndex > flowedPage.startIndex;
|
|
15052
|
+
if (!sectionOverlapsPage) {
|
|
15053
|
+
return;
|
|
15054
|
+
}
|
|
15055
|
+
const hasStart = startInfo !== null;
|
|
15056
|
+
const hasEnd = endInfo !== null;
|
|
15057
|
+
const startsBeforePage = section.startIndex < flowedPage.startIndex;
|
|
15058
|
+
const endsAfterPage = section.endIndex > flowedPage.endIndex;
|
|
15059
|
+
ctx.save();
|
|
15060
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
15061
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
15062
|
+
ctx.lineWidth = 1;
|
|
15063
|
+
// Position on the right side of the content area
|
|
15064
|
+
const labelWidth = 22;
|
|
15065
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
15066
|
+
const connectorX = labelX + labelWidth / 2;
|
|
15067
|
+
// Draw start indicator lines
|
|
15068
|
+
if (hasStart) {
|
|
15069
|
+
const startY = startInfo.y;
|
|
15070
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15071
|
+
ctx.beginPath();
|
|
15072
|
+
ctx.moveTo(contentBounds.x, startY);
|
|
15073
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, startY);
|
|
15074
|
+
ctx.stroke();
|
|
15075
|
+
ctx.setLineDash([]);
|
|
15076
|
+
ctx.beginPath();
|
|
15077
|
+
ctx.moveTo(contentBounds.x + contentBounds.width, startY);
|
|
15078
|
+
ctx.lineTo(labelX, startY);
|
|
15079
|
+
ctx.stroke();
|
|
15080
|
+
}
|
|
15081
|
+
else if (startsBeforePage) {
|
|
15082
|
+
const topY = contentBounds.y;
|
|
15083
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15084
|
+
ctx.beginPath();
|
|
15085
|
+
ctx.moveTo(contentBounds.x, topY);
|
|
15086
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15087
|
+
ctx.stroke();
|
|
15088
|
+
ctx.setLineDash([]);
|
|
15089
|
+
ctx.beginPath();
|
|
15090
|
+
ctx.moveTo(connectorX, topY);
|
|
15091
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, topY);
|
|
15092
|
+
ctx.stroke();
|
|
15093
|
+
}
|
|
15094
|
+
// Draw end indicator
|
|
15095
|
+
if (hasEnd) {
|
|
15096
|
+
const endY = endInfo.y;
|
|
15097
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15098
|
+
ctx.beginPath();
|
|
15099
|
+
ctx.moveTo(contentBounds.x, endY);
|
|
15100
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15101
|
+
ctx.stroke();
|
|
15102
|
+
ctx.setLineDash([]);
|
|
15103
|
+
ctx.beginPath();
|
|
15104
|
+
ctx.moveTo(connectorX, endY);
|
|
15105
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, endY);
|
|
15106
|
+
ctx.stroke();
|
|
15107
|
+
}
|
|
15108
|
+
else if (endsAfterPage) {
|
|
15109
|
+
const bottomY = contentBounds.y + contentBounds.height;
|
|
15110
|
+
ctx.setLineDash(COND_LINE_DASH);
|
|
15111
|
+
ctx.beginPath();
|
|
15112
|
+
ctx.moveTo(contentBounds.x, bottomY);
|
|
15113
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15114
|
+
ctx.stroke();
|
|
15115
|
+
ctx.setLineDash([]);
|
|
15116
|
+
ctx.beginPath();
|
|
15117
|
+
ctx.moveTo(connectorX, bottomY);
|
|
15118
|
+
ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
|
|
15119
|
+
ctx.stroke();
|
|
15120
|
+
}
|
|
15121
|
+
// Draw vertical connector line
|
|
15122
|
+
let verticalStartY;
|
|
15123
|
+
let verticalEndY;
|
|
15124
|
+
if (hasStart) {
|
|
15125
|
+
verticalStartY = startInfo.y;
|
|
15126
|
+
}
|
|
15127
|
+
else if (startsBeforePage) {
|
|
15128
|
+
verticalStartY = contentBounds.y;
|
|
15129
|
+
}
|
|
15130
|
+
else {
|
|
15131
|
+
verticalStartY = contentBounds.y;
|
|
15132
|
+
}
|
|
15133
|
+
if (hasEnd) {
|
|
15134
|
+
verticalEndY = endInfo.y;
|
|
15135
|
+
}
|
|
15136
|
+
else if (endsAfterPage) {
|
|
13777
15137
|
verticalEndY = contentBounds.y + contentBounds.height;
|
|
13778
15138
|
}
|
|
13779
15139
|
else {
|
|
13780
|
-
verticalEndY = verticalStartY;
|
|
15140
|
+
verticalEndY = verticalStartY;
|
|
13781
15141
|
}
|
|
13782
15142
|
if (verticalEndY > verticalStartY) {
|
|
13783
15143
|
ctx.beginPath();
|
|
@@ -13785,12 +15145,12 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13785
15145
|
ctx.lineTo(connectorX, verticalEndY);
|
|
13786
15146
|
ctx.stroke();
|
|
13787
15147
|
}
|
|
13788
|
-
// Draw "
|
|
15148
|
+
// Draw "If" label
|
|
13789
15149
|
if (hasStart) {
|
|
13790
15150
|
const startY = startInfo.y;
|
|
13791
|
-
this.
|
|
15151
|
+
this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
|
|
13792
15152
|
}
|
|
13793
|
-
// Update
|
|
15153
|
+
// Update visual state
|
|
13794
15154
|
section.visualState = {
|
|
13795
15155
|
startPageIndex: hasStart ? pageIndex : -1,
|
|
13796
15156
|
startY: hasStart ? startInfo.y : 0,
|
|
@@ -13801,111 +15161,52 @@ class FlowingTextRenderer extends EventEmitter {
|
|
|
13801
15161
|
ctx.restore();
|
|
13802
15162
|
}
|
|
13803
15163
|
/**
|
|
13804
|
-
* Draw the "
|
|
13805
|
-
* When not selected, draws an outlined rectangle.
|
|
13806
|
-
* When selected, draws a filled rectangle.
|
|
15164
|
+
* Draw the "If" label in a rounded rectangle.
|
|
13807
15165
|
*/
|
|
13808
|
-
|
|
15166
|
+
drawCondLabel(ctx, x, y, text, isSelected = false) {
|
|
13809
15167
|
ctx.save();
|
|
13810
15168
|
ctx.font = '10px Arial';
|
|
13811
15169
|
const metrics = ctx.measureText(text);
|
|
13812
15170
|
const textWidth = metrics.width;
|
|
13813
15171
|
const textHeight = 10;
|
|
13814
|
-
const boxWidth = textWidth +
|
|
13815
|
-
const boxHeight = textHeight +
|
|
15172
|
+
const boxWidth = textWidth + COND_LABEL_PADDING * 2;
|
|
15173
|
+
const boxHeight = textHeight + COND_LABEL_PADDING * 2;
|
|
13816
15174
|
ctx.beginPath();
|
|
13817
|
-
this.roundRect(ctx, x, y, boxWidth, boxHeight,
|
|
15175
|
+
this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
|
|
13818
15176
|
if (isSelected) {
|
|
13819
|
-
|
|
13820
|
-
ctx.fillStyle = LOOP_INDICATOR_COLOR;
|
|
15177
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
13821
15178
|
ctx.fill();
|
|
13822
15179
|
ctx.fillStyle = '#ffffff';
|
|
13823
15180
|
}
|
|
13824
15181
|
else {
|
|
13825
|
-
// Not selected: white background, outlined with colored text
|
|
13826
15182
|
ctx.fillStyle = '#ffffff';
|
|
13827
15183
|
ctx.fill();
|
|
13828
|
-
ctx.strokeStyle =
|
|
15184
|
+
ctx.strokeStyle = COND_INDICATOR_COLOR;
|
|
13829
15185
|
ctx.lineWidth = 1.5;
|
|
13830
15186
|
ctx.stroke();
|
|
13831
|
-
ctx.fillStyle =
|
|
15187
|
+
ctx.fillStyle = COND_INDICATOR_COLOR;
|
|
13832
15188
|
}
|
|
13833
|
-
// Draw text
|
|
13834
15189
|
ctx.textBaseline = 'middle';
|
|
13835
|
-
ctx.fillText(text, x +
|
|
15190
|
+
ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
|
|
13836
15191
|
ctx.restore();
|
|
13837
15192
|
}
|
|
13838
15193
|
/**
|
|
13839
|
-
*
|
|
13840
|
-
*/
|
|
13841
|
-
roundRect(ctx, x, y, width, height, radius) {
|
|
13842
|
-
ctx.moveTo(x + radius, y);
|
|
13843
|
-
ctx.lineTo(x + width - radius, y);
|
|
13844
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
13845
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
13846
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
13847
|
-
ctx.lineTo(x + radius, y + height);
|
|
13848
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
13849
|
-
ctx.lineTo(x, y + radius);
|
|
13850
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
13851
|
-
ctx.closePath();
|
|
13852
|
-
}
|
|
13853
|
-
/**
|
|
13854
|
-
* Find the Y position for a text index on a flowed page.
|
|
13855
|
-
* Returns the Y position at the TOP of the line containing the text index.
|
|
13856
|
-
*/
|
|
13857
|
-
findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
|
|
13858
|
-
let y = contentBounds.y;
|
|
13859
|
-
for (let i = 0; i < flowedPage.lines.length; i++) {
|
|
13860
|
-
const line = flowedPage.lines[i];
|
|
13861
|
-
// Check if this line contains the text index
|
|
13862
|
-
if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
|
|
13863
|
-
return { y, lineIndex: i };
|
|
13864
|
-
}
|
|
13865
|
-
// Check if text index is exactly at the start of this line
|
|
13866
|
-
// (for section boundaries that are at paragraph starts)
|
|
13867
|
-
if (textIndex === line.startIndex) {
|
|
13868
|
-
return { y, lineIndex: i };
|
|
13869
|
-
}
|
|
13870
|
-
y += line.height;
|
|
13871
|
-
}
|
|
13872
|
-
// Check if text index is just past the last line (end of content)
|
|
13873
|
-
if (flowedPage.lines.length > 0) {
|
|
13874
|
-
const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
|
|
13875
|
-
if (textIndex === lastLine.endIndex + 1) {
|
|
13876
|
-
return { y, lineIndex: flowedPage.lines.length - 1 };
|
|
13877
|
-
}
|
|
13878
|
-
}
|
|
13879
|
-
return null;
|
|
13880
|
-
}
|
|
13881
|
-
/**
|
|
13882
|
-
* Check if a section spans across a flowed page (starts before and ends after).
|
|
13883
|
-
*/
|
|
13884
|
-
sectionSpansPage(section, flowedPage) {
|
|
13885
|
-
if (flowedPage.lines.length === 0)
|
|
13886
|
-
return false;
|
|
13887
|
-
const pageStart = flowedPage.startIndex;
|
|
13888
|
-
const pageEnd = flowedPage.endIndex;
|
|
13889
|
-
// Section spans this page if it started before and ends after
|
|
13890
|
-
return section.startIndex < pageStart && section.endIndex > pageEnd;
|
|
13891
|
-
}
|
|
13892
|
-
/**
|
|
13893
|
-
* Get a repeating section at a point (for click detection).
|
|
13894
|
-
* Checks if the point is on the Loop label or vertical connector.
|
|
15194
|
+
* Get a conditional section at a point (for click detection).
|
|
13895
15195
|
*/
|
|
13896
|
-
|
|
13897
|
-
const
|
|
13898
|
-
const
|
|
15196
|
+
getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
|
|
15197
|
+
const labelWidth = 22;
|
|
15198
|
+
const labelX = contentBounds.x + contentBounds.width + 5;
|
|
13899
15199
|
const connectorX = labelX + labelWidth / 2;
|
|
13900
|
-
const hitRadius = 10;
|
|
15200
|
+
const hitRadius = 10;
|
|
13901
15201
|
for (const section of sections) {
|
|
13902
15202
|
const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
|
|
13903
15203
|
const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
|
|
13904
|
-
const sectionSpansThisPage =
|
|
15204
|
+
const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
|
|
15205
|
+
section.endIndex > flowedPage.endIndex;
|
|
13905
15206
|
if (!startInfo && !endInfo && !sectionSpansThisPage) {
|
|
13906
15207
|
continue;
|
|
13907
15208
|
}
|
|
13908
|
-
// Check if click is on the
|
|
15209
|
+
// Check if click is on the "If" label
|
|
13909
15210
|
if (startInfo) {
|
|
13910
15211
|
const labelY = startInfo.y - 10;
|
|
13911
15212
|
const labelHeight = 18;
|
|
@@ -13970,6 +15271,7 @@ class CanvasManager extends EventEmitter {
|
|
|
13970
15271
|
this.isSelectingText = false;
|
|
13971
15272
|
this.textSelectionStartPageId = null;
|
|
13972
15273
|
this.selectedSectionId = null;
|
|
15274
|
+
this.selectedConditionalSectionId = null;
|
|
13973
15275
|
this._activeSection = 'body';
|
|
13974
15276
|
this.lastClickTime = 0;
|
|
13975
15277
|
this.lastClickPosition = null;
|
|
@@ -14109,6 +15411,11 @@ class CanvasManager extends EventEmitter {
|
|
|
14109
15411
|
}
|
|
14110
15412
|
// 2. CONTENT: Render all text and elements
|
|
14111
15413
|
const pageIndex = this.document.pages.findIndex(p => p.id === page.id);
|
|
15414
|
+
// Clear content hit targets before rendering all sections (header, body, footer)
|
|
15415
|
+
// so that each section's hit targets are re-registered during render
|
|
15416
|
+
if (pageIndex === 0) {
|
|
15417
|
+
this.flowingTextRenderer.hitTestManager.clearCategory('content');
|
|
15418
|
+
}
|
|
14112
15419
|
// Render header content
|
|
14113
15420
|
const headerRegion = this.regionManager.getHeaderRegion();
|
|
14114
15421
|
this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
|
|
@@ -14136,6 +15443,16 @@ class CanvasManager extends EventEmitter {
|
|
|
14136
15443
|
this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
|
|
14137
15444
|
}
|
|
14138
15445
|
}
|
|
15446
|
+
// Render conditional section indicators (only in body)
|
|
15447
|
+
const condSections = bodyFlowingContent?.getConditionalSections() ?? [];
|
|
15448
|
+
if (condSections.length > 0) {
|
|
15449
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
15450
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
15451
|
+
const pageDimensions = page.getPageDimensions();
|
|
15452
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
15453
|
+
this.flowingTextRenderer.renderConditionalSectionIndicators(condSections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedConditionalSectionId);
|
|
15454
|
+
}
|
|
15455
|
+
}
|
|
14139
15456
|
// Render all elements (without selection marks)
|
|
14140
15457
|
this.renderPageElements(page, ctx);
|
|
14141
15458
|
// 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
|
|
@@ -14433,11 +15750,10 @@ class CanvasManager extends EventEmitter {
|
|
|
14433
15750
|
const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
|
|
14434
15751
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14435
15752
|
const object = embeddedObjectHit.data.object;
|
|
14436
|
-
//
|
|
15753
|
+
// If object is in a different section, switch to that section first
|
|
14437
15754
|
const objectSection = this.getSectionForEmbeddedObject(object);
|
|
14438
15755
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14439
|
-
|
|
14440
|
-
return;
|
|
15756
|
+
this.setActiveSection(objectSection);
|
|
14441
15757
|
}
|
|
14442
15758
|
// For relative-positioned objects, prepare for potential drag
|
|
14443
15759
|
// Don't start drag immediately - wait for threshold to allow double-click
|
|
@@ -14924,14 +16240,12 @@ class CanvasManager extends EventEmitter {
|
|
|
14924
16240
|
const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
|
|
14925
16241
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
14926
16242
|
const clickedObject = embeddedObjectHit.data.object;
|
|
14927
|
-
//
|
|
16243
|
+
// If object is in a different section, switch to that section first
|
|
14928
16244
|
const objectSection = this.getSectionForEmbeddedObject(clickedObject);
|
|
14929
|
-
// Only allow selection if object is in the active section
|
|
14930
16245
|
if (objectSection && objectSection !== this._activeSection) {
|
|
14931
|
-
|
|
14932
|
-
return;
|
|
16246
|
+
this.setActiveSection(objectSection);
|
|
14933
16247
|
}
|
|
14934
|
-
// Clicked on embedded object
|
|
16248
|
+
// Clicked on embedded object - clear text selection and select it
|
|
14935
16249
|
const activeFlowingContent = this.getFlowingContentForActiveSection();
|
|
14936
16250
|
if (activeFlowingContent) {
|
|
14937
16251
|
activeFlowingContent.clearSelection();
|
|
@@ -14968,6 +16282,64 @@ class CanvasManager extends EventEmitter {
|
|
|
14968
16282
|
}
|
|
14969
16283
|
}
|
|
14970
16284
|
}
|
|
16285
|
+
// Check if we clicked on a conditional section indicator
|
|
16286
|
+
if (bodyFlowingContent) {
|
|
16287
|
+
const condSections = bodyFlowingContent.getConditionalSections();
|
|
16288
|
+
if (condSections.length > 0 && page) {
|
|
16289
|
+
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
16290
|
+
const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
|
|
16291
|
+
if (flowedPages && flowedPages[pageIndex]) {
|
|
16292
|
+
const contentBounds = page.getContentBounds();
|
|
16293
|
+
const contentRect = {
|
|
16294
|
+
x: contentBounds.position.x,
|
|
16295
|
+
y: contentBounds.position.y,
|
|
16296
|
+
width: contentBounds.size.width,
|
|
16297
|
+
height: contentBounds.size.height
|
|
16298
|
+
};
|
|
16299
|
+
const pageDimensions = page.getPageDimensions();
|
|
16300
|
+
const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
|
|
16301
|
+
const clickedCondSection = this.flowingTextRenderer.getConditionalSectionAtPoint(point, condSections, pageIndex, pageBounds, contentRect, flowedPages[pageIndex]);
|
|
16302
|
+
if (clickedCondSection) {
|
|
16303
|
+
this.clearSelection();
|
|
16304
|
+
this.selectedConditionalSectionId = clickedCondSection.id;
|
|
16305
|
+
this.render();
|
|
16306
|
+
this.emit('conditional-section-clicked', { section: clickedCondSection });
|
|
16307
|
+
return;
|
|
16308
|
+
}
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
// Check if we clicked on a table row loop label
|
|
16313
|
+
const clickedPageIdx = this.document.pages.findIndex(p => p.id === pageId);
|
|
16314
|
+
const bodyContent = this.document.bodyFlowingContent;
|
|
16315
|
+
if (bodyContent) {
|
|
16316
|
+
const embeddedObjects = bodyContent.getEmbeddedObjects();
|
|
16317
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
16318
|
+
if (obj instanceof TableObject && obj.renderedPosition && obj.renderedPageIndex === clickedPageIdx) {
|
|
16319
|
+
// Convert to table-local coordinates
|
|
16320
|
+
const localPoint = {
|
|
16321
|
+
x: point.x - obj.renderedPosition.x,
|
|
16322
|
+
y: point.y - obj.renderedPosition.y
|
|
16323
|
+
};
|
|
16324
|
+
const clickedLoop = obj.getRowLoopAtPoint(localPoint);
|
|
16325
|
+
if (clickedLoop) {
|
|
16326
|
+
// Select this loop
|
|
16327
|
+
obj.selectRowLoop(clickedLoop.id);
|
|
16328
|
+
this.render();
|
|
16329
|
+
this.emit('table-row-loop-clicked', { table: obj, loop: clickedLoop });
|
|
16330
|
+
return;
|
|
16331
|
+
}
|
|
16332
|
+
// Check for row conditional click
|
|
16333
|
+
const clickedCond = obj.getRowConditionalAtPoint(localPoint);
|
|
16334
|
+
if (clickedCond) {
|
|
16335
|
+
obj.selectRowConditional(clickedCond.id);
|
|
16336
|
+
this.render();
|
|
16337
|
+
this.emit('table-row-conditional-clicked', { table: obj, conditional: clickedCond });
|
|
16338
|
+
return;
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
}
|
|
16342
|
+
}
|
|
14971
16343
|
// If no regular element was clicked, try flowing text using unified region click handler
|
|
14972
16344
|
const ctx = this.contexts.get(pageId);
|
|
14973
16345
|
const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
|
|
@@ -15177,26 +16549,21 @@ class CanvasManager extends EventEmitter {
|
|
|
15177
16549
|
const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
|
|
15178
16550
|
if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
|
|
15179
16551
|
const object = embeddedObjectHit.data.object;
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
if (objectSection && objectSection !== this._activeSection) ;
|
|
15183
|
-
else {
|
|
15184
|
-
if (object.position === 'relative') {
|
|
15185
|
-
canvas.style.cursor = 'move';
|
|
15186
|
-
return;
|
|
15187
|
-
}
|
|
15188
|
-
// Show text cursor for objects in edit mode, arrow otherwise
|
|
15189
|
-
if (object instanceof TextBoxObject && this.editingTextBox === object) {
|
|
15190
|
-
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15191
|
-
}
|
|
15192
|
-
else if (object instanceof TableObject && this._focusedControl === object) {
|
|
15193
|
-
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
15194
|
-
}
|
|
15195
|
-
else {
|
|
15196
|
-
canvas.style.cursor = 'default';
|
|
15197
|
-
}
|
|
16552
|
+
if (object.position === 'relative') {
|
|
16553
|
+
canvas.style.cursor = 'move';
|
|
15198
16554
|
return;
|
|
15199
16555
|
}
|
|
16556
|
+
// Show text cursor for objects in edit mode, arrow otherwise
|
|
16557
|
+
if (object instanceof TextBoxObject && this.editingTextBox === object) {
|
|
16558
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16559
|
+
}
|
|
16560
|
+
else if (object instanceof TableObject && this._focusedControl === object) {
|
|
16561
|
+
canvas.style.cursor = CanvasManager.TEXT_CURSOR;
|
|
16562
|
+
}
|
|
16563
|
+
else {
|
|
16564
|
+
canvas.style.cursor = 'default';
|
|
16565
|
+
}
|
|
16566
|
+
return;
|
|
15200
16567
|
}
|
|
15201
16568
|
// Check for table cells (show text cursor)
|
|
15202
16569
|
const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
|
|
@@ -15386,6 +16753,7 @@ class CanvasManager extends EventEmitter {
|
|
|
15386
16753
|
});
|
|
15387
16754
|
this.selectedElements.clear();
|
|
15388
16755
|
this.selectedSectionId = null;
|
|
16756
|
+
this.selectedConditionalSectionId = null;
|
|
15389
16757
|
Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
|
|
15390
16758
|
this.render();
|
|
15391
16759
|
this.updateResizeHandleHitTargets();
|
|
@@ -16468,8 +17836,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
|
|
|
16468
17836
|
* - Repeating section indicators, loop markers
|
|
16469
17837
|
*/
|
|
16470
17838
|
class PDFGenerator {
|
|
16471
|
-
constructor() {
|
|
17839
|
+
constructor(fontManager) {
|
|
16472
17840
|
this.fontCache = new Map();
|
|
17841
|
+
this.customFontCache = new Map();
|
|
17842
|
+
this.fontManager = fontManager;
|
|
16473
17843
|
}
|
|
16474
17844
|
/**
|
|
16475
17845
|
* Generate a PDF from the document.
|
|
@@ -16480,9 +17850,13 @@ class PDFGenerator {
|
|
|
16480
17850
|
*/
|
|
16481
17851
|
async generate(document, flowedContent, _options) {
|
|
16482
17852
|
const pdfDoc = await PDFDocument.create();
|
|
17853
|
+
pdfDoc.registerFontkit(fontkit);
|
|
16483
17854
|
this.fontCache.clear();
|
|
17855
|
+
this.customFontCache.clear();
|
|
16484
17856
|
// Embed standard fonts we'll need
|
|
16485
17857
|
await this.embedStandardFonts(pdfDoc);
|
|
17858
|
+
// Embed any custom fonts that have font data
|
|
17859
|
+
await this.embedCustomFonts(pdfDoc);
|
|
16486
17860
|
// Render each page
|
|
16487
17861
|
for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
|
|
16488
17862
|
try {
|
|
@@ -16604,11 +17978,59 @@ class PDFGenerator {
|
|
|
16604
17978
|
}
|
|
16605
17979
|
return result;
|
|
16606
17980
|
}
|
|
17981
|
+
/**
|
|
17982
|
+
* Embed custom fonts that have raw font data available.
|
|
17983
|
+
*/
|
|
17984
|
+
async embedCustomFonts(pdfDoc) {
|
|
17985
|
+
const fonts = this.fontManager.getAvailableFonts();
|
|
17986
|
+
for (const font of fonts) {
|
|
17987
|
+
if (font.source !== 'custom')
|
|
17988
|
+
continue;
|
|
17989
|
+
for (const variant of font.variants) {
|
|
17990
|
+
if (!variant.fontData)
|
|
17991
|
+
continue;
|
|
17992
|
+
const cacheKey = `custom:${font.family.toLowerCase()}:${variant.weight}:${variant.style}`;
|
|
17993
|
+
try {
|
|
17994
|
+
// Ensure we pass Uint8Array (some pdf-lib versions need it)
|
|
17995
|
+
const fontBytes = variant.fontData instanceof Uint8Array
|
|
17996
|
+
? variant.fontData
|
|
17997
|
+
: new Uint8Array(variant.fontData);
|
|
17998
|
+
const embedded = await pdfDoc.embedFont(fontBytes, { subset: true });
|
|
17999
|
+
this.customFontCache.set(cacheKey, embedded);
|
|
18000
|
+
Logger.log('[pc-editor:PDFGenerator] Embedded custom font:', font.family, variant.weight, variant.style);
|
|
18001
|
+
}
|
|
18002
|
+
catch (e) {
|
|
18003
|
+
Logger.warn('[pc-editor:PDFGenerator] Failed to embed custom font:', font.family, e);
|
|
18004
|
+
}
|
|
18005
|
+
}
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
18008
|
+
/**
|
|
18009
|
+
* Check if a font family is a custom font with embedded data.
|
|
18010
|
+
*/
|
|
18011
|
+
isCustomFont(family) {
|
|
18012
|
+
return !this.fontManager.isBuiltIn(family) && this.fontManager.isRegistered(family);
|
|
18013
|
+
}
|
|
16607
18014
|
/**
|
|
16608
18015
|
* Get a font from cache by formatting style.
|
|
18016
|
+
* Checks custom fonts first, then falls back to standard fonts.
|
|
16609
18017
|
*/
|
|
16610
18018
|
getFont(formatting) {
|
|
16611
|
-
const
|
|
18019
|
+
const family = formatting.fontFamily || 'Arial';
|
|
18020
|
+
const weight = formatting.fontWeight || 'normal';
|
|
18021
|
+
const style = formatting.fontStyle || 'normal';
|
|
18022
|
+
// Try custom font first
|
|
18023
|
+
const customKey = `custom:${family.toLowerCase()}:${weight}:${style}`;
|
|
18024
|
+
const customFont = this.customFontCache.get(customKey);
|
|
18025
|
+
if (customFont)
|
|
18026
|
+
return customFont;
|
|
18027
|
+
// Try custom font with normal variant as fallback
|
|
18028
|
+
const customNormalKey = `custom:${family.toLowerCase()}:normal:normal`;
|
|
18029
|
+
const customNormalFont = this.customFontCache.get(customNormalKey);
|
|
18030
|
+
if (customNormalFont)
|
|
18031
|
+
return customNormalFont;
|
|
18032
|
+
// Fall back to standard fonts
|
|
18033
|
+
const standardFont = getStandardFont(family, weight, style);
|
|
16612
18034
|
return this.fontCache.get(standardFont) || this.fontCache.get(StandardFonts.Helvetica);
|
|
16613
18035
|
}
|
|
16614
18036
|
/**
|
|
@@ -16641,12 +18063,14 @@ class PDFGenerator {
|
|
|
16641
18063
|
for (const run of line.runs) {
|
|
16642
18064
|
if (!run.text)
|
|
16643
18065
|
continue;
|
|
16644
|
-
// Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
|
|
16645
|
-
const safeText = this.filterToWinAnsi(run.text);
|
|
16646
|
-
if (!safeText)
|
|
16647
|
-
continue;
|
|
16648
18066
|
// Ensure formatting has required properties with defaults
|
|
16649
18067
|
const formatting = run.formatting || {};
|
|
18068
|
+
// Custom fonts support full Unicode; standard fonts need WinAnsi filtering
|
|
18069
|
+
const safeText = this.isCustomFont(formatting.fontFamily || 'Arial')
|
|
18070
|
+
? run.text
|
|
18071
|
+
: this.filterToWinAnsi(run.text);
|
|
18072
|
+
if (!safeText)
|
|
18073
|
+
continue;
|
|
16650
18074
|
const font = this.getFont(formatting);
|
|
16651
18075
|
const fontSize = formatting.fontSize || 14;
|
|
16652
18076
|
const color = parseColor(formatting.color || '#000000');
|
|
@@ -21321,6 +22745,156 @@ class PDFImporter {
|
|
|
21321
22745
|
}
|
|
21322
22746
|
}
|
|
21323
22747
|
|
|
22748
|
+
/**
|
|
22749
|
+
* FontManager - Manages font registration and availability for the editor.
|
|
22750
|
+
*
|
|
22751
|
+
* Built-in fonts are web-safe and map to pdf-lib StandardFonts.
|
|
22752
|
+
* Custom fonts are loaded via the FontFace API for canvas rendering
|
|
22753
|
+
* and their raw bytes are stored for PDF embedding.
|
|
22754
|
+
*/
|
|
22755
|
+
/**
|
|
22756
|
+
* Built-in web-safe fonts that need no loading.
|
|
22757
|
+
*/
|
|
22758
|
+
const BUILT_IN_FONTS = [
|
|
22759
|
+
'Arial',
|
|
22760
|
+
'Times New Roman',
|
|
22761
|
+
'Courier New',
|
|
22762
|
+
'Georgia',
|
|
22763
|
+
'Verdana'
|
|
22764
|
+
];
|
|
22765
|
+
class FontManager extends EventEmitter {
|
|
22766
|
+
constructor() {
|
|
22767
|
+
super();
|
|
22768
|
+
this.fonts = new Map();
|
|
22769
|
+
// Register built-in fonts
|
|
22770
|
+
for (const family of BUILT_IN_FONTS) {
|
|
22771
|
+
this.fonts.set(family.toLowerCase(), {
|
|
22772
|
+
family,
|
|
22773
|
+
source: 'built-in',
|
|
22774
|
+
variants: [{
|
|
22775
|
+
weight: 'normal',
|
|
22776
|
+
style: 'normal',
|
|
22777
|
+
fontData: null,
|
|
22778
|
+
loaded: true
|
|
22779
|
+
}]
|
|
22780
|
+
});
|
|
22781
|
+
}
|
|
22782
|
+
}
|
|
22783
|
+
/**
|
|
22784
|
+
* Register a custom font. Fetches the font data if a URL is provided,
|
|
22785
|
+
* creates a FontFace for canvas rendering, and stores the raw bytes for PDF embedding.
|
|
22786
|
+
*/
|
|
22787
|
+
async registerFont(options) {
|
|
22788
|
+
const { family, url, data, weight = 'normal', style = 'normal' } = options;
|
|
22789
|
+
Logger.log('[pc-editor:FontManager] registerFont', family, weight, style);
|
|
22790
|
+
let fontData = null;
|
|
22791
|
+
// Get font bytes
|
|
22792
|
+
if (data) {
|
|
22793
|
+
fontData = data;
|
|
22794
|
+
}
|
|
22795
|
+
else if (url) {
|
|
22796
|
+
try {
|
|
22797
|
+
const response = await fetch(url);
|
|
22798
|
+
if (!response.ok) {
|
|
22799
|
+
throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`);
|
|
22800
|
+
}
|
|
22801
|
+
fontData = await response.arrayBuffer();
|
|
22802
|
+
}
|
|
22803
|
+
catch (e) {
|
|
22804
|
+
Logger.error(`[pc-editor:FontManager] Failed to fetch font "${family}" from ${url}:`, e);
|
|
22805
|
+
throw e;
|
|
22806
|
+
}
|
|
22807
|
+
}
|
|
22808
|
+
// Create FontFace for canvas rendering
|
|
22809
|
+
if (fontData && typeof FontFace !== 'undefined') {
|
|
22810
|
+
try {
|
|
22811
|
+
const fontFace = new FontFace(family, fontData, {
|
|
22812
|
+
weight,
|
|
22813
|
+
style
|
|
22814
|
+
});
|
|
22815
|
+
await fontFace.load();
|
|
22816
|
+
document.fonts.add(fontFace);
|
|
22817
|
+
Logger.log('[pc-editor:FontManager] FontFace loaded:', family, weight, style);
|
|
22818
|
+
}
|
|
22819
|
+
catch (e) {
|
|
22820
|
+
Logger.error(`[pc-editor:FontManager] Failed to load FontFace "${family}":`, e);
|
|
22821
|
+
throw e;
|
|
22822
|
+
}
|
|
22823
|
+
}
|
|
22824
|
+
// Register in our map
|
|
22825
|
+
const key = family.toLowerCase();
|
|
22826
|
+
let registration = this.fonts.get(key);
|
|
22827
|
+
if (!registration) {
|
|
22828
|
+
registration = {
|
|
22829
|
+
family,
|
|
22830
|
+
source: 'custom',
|
|
22831
|
+
variants: []
|
|
22832
|
+
};
|
|
22833
|
+
this.fonts.set(key, registration);
|
|
22834
|
+
}
|
|
22835
|
+
else if (registration.source === 'built-in') {
|
|
22836
|
+
// Upgrading a built-in font with custom data (e.g., for PDF embedding)
|
|
22837
|
+
registration.source = 'custom';
|
|
22838
|
+
}
|
|
22839
|
+
// Add or update variant
|
|
22840
|
+
const existingVariant = registration.variants.find(v => v.weight === weight && v.style === style);
|
|
22841
|
+
if (existingVariant) {
|
|
22842
|
+
existingVariant.fontData = fontData;
|
|
22843
|
+
existingVariant.loaded = true;
|
|
22844
|
+
}
|
|
22845
|
+
else {
|
|
22846
|
+
registration.variants.push({
|
|
22847
|
+
weight,
|
|
22848
|
+
style,
|
|
22849
|
+
fontData,
|
|
22850
|
+
loaded: true
|
|
22851
|
+
});
|
|
22852
|
+
}
|
|
22853
|
+
this.emit('font-registered', { family, weight, style });
|
|
22854
|
+
}
|
|
22855
|
+
/**
|
|
22856
|
+
* Get all registered font families.
|
|
22857
|
+
*/
|
|
22858
|
+
getAvailableFonts() {
|
|
22859
|
+
return Array.from(this.fonts.values());
|
|
22860
|
+
}
|
|
22861
|
+
/**
|
|
22862
|
+
* Get all available font family names.
|
|
22863
|
+
*/
|
|
22864
|
+
getAvailableFontFamilies() {
|
|
22865
|
+
return Array.from(this.fonts.values()).map(f => f.family);
|
|
22866
|
+
}
|
|
22867
|
+
/**
|
|
22868
|
+
* Check if a font family is built-in.
|
|
22869
|
+
*/
|
|
22870
|
+
isBuiltIn(family) {
|
|
22871
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22872
|
+
return reg?.source === 'built-in';
|
|
22873
|
+
}
|
|
22874
|
+
/**
|
|
22875
|
+
* Check if a font family is registered (built-in or custom).
|
|
22876
|
+
*/
|
|
22877
|
+
isRegistered(family) {
|
|
22878
|
+
return this.fonts.has(family.toLowerCase());
|
|
22879
|
+
}
|
|
22880
|
+
/**
|
|
22881
|
+
* Get raw font bytes for PDF embedding.
|
|
22882
|
+
* Returns null for built-in fonts (they use StandardFonts in pdf-lib).
|
|
22883
|
+
*/
|
|
22884
|
+
getFontData(family, weight = 'normal', style = 'normal') {
|
|
22885
|
+
const reg = this.fonts.get(family.toLowerCase());
|
|
22886
|
+
if (!reg)
|
|
22887
|
+
return null;
|
|
22888
|
+
// Try exact match first
|
|
22889
|
+
const exact = reg.variants.find(v => v.weight === weight && v.style === style);
|
|
22890
|
+
if (exact?.fontData)
|
|
22891
|
+
return exact.fontData;
|
|
22892
|
+
// Fall back to normal variant
|
|
22893
|
+
const normal = reg.variants.find(v => v.weight === 'normal' && v.style === 'normal');
|
|
22894
|
+
return normal?.fontData || null;
|
|
22895
|
+
}
|
|
22896
|
+
}
|
|
22897
|
+
|
|
21324
22898
|
class PCEditor extends EventEmitter {
|
|
21325
22899
|
constructor(container, options) {
|
|
21326
22900
|
super();
|
|
@@ -21347,7 +22921,8 @@ class PCEditor extends EventEmitter {
|
|
|
21347
22921
|
units: this.options.units
|
|
21348
22922
|
});
|
|
21349
22923
|
this.dataBinder = new DataBinder();
|
|
21350
|
-
this.
|
|
22924
|
+
this.fontManager = new FontManager();
|
|
22925
|
+
this.pdfGenerator = new PDFGenerator(this.fontManager);
|
|
21351
22926
|
this.clipboardManager = new ClipboardManager();
|
|
21352
22927
|
this.initialize();
|
|
21353
22928
|
}
|
|
@@ -21513,6 +23088,10 @@ class PCEditor extends EventEmitter {
|
|
|
21513
23088
|
this.canvasManager.on('table-cell-selection-changed', (data) => {
|
|
21514
23089
|
this.emit('table-cell-selection-changed', data);
|
|
21515
23090
|
});
|
|
23091
|
+
// Forward table row loop clicks
|
|
23092
|
+
this.canvasManager.on('table-row-loop-clicked', (data) => {
|
|
23093
|
+
this.emit('table-row-loop-clicked', data);
|
|
23094
|
+
});
|
|
21516
23095
|
this.canvasManager.on('repeating-section-clicked', (data) => {
|
|
21517
23096
|
// Repeating section clicked - update selection state
|
|
21518
23097
|
if (data.section && data.section.id) {
|
|
@@ -21523,6 +23102,16 @@ class PCEditor extends EventEmitter {
|
|
|
21523
23102
|
this.emitSelectionChange();
|
|
21524
23103
|
}
|
|
21525
23104
|
});
|
|
23105
|
+
this.canvasManager.on('conditional-section-clicked', (data) => {
|
|
23106
|
+
// Conditional section clicked - update selection state
|
|
23107
|
+
if (data.section && data.section.id) {
|
|
23108
|
+
this.currentSelection = {
|
|
23109
|
+
type: 'conditional-section',
|
|
23110
|
+
sectionId: data.section.id
|
|
23111
|
+
};
|
|
23112
|
+
this.emitSelectionChange();
|
|
23113
|
+
}
|
|
23114
|
+
});
|
|
21526
23115
|
// Listen for section focus changes from CanvasManager (double-click)
|
|
21527
23116
|
this.canvasManager.on('section-focus-changed', (data) => {
|
|
21528
23117
|
// Update our internal state to match the canvas manager
|
|
@@ -22351,17 +23940,24 @@ class PCEditor extends EventEmitter {
|
|
|
22351
23940
|
this.selectAll();
|
|
22352
23941
|
return;
|
|
22353
23942
|
}
|
|
22354
|
-
// If an embedded object is selected (but not being edited),
|
|
22355
|
-
|
|
22356
|
-
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
22357
|
-
if (isArrowKey && this.canvasManager.hasSelectedElements()) {
|
|
22358
|
-
// Check if we're not in editing mode
|
|
23943
|
+
// If an embedded object is selected (but not being edited), handle special keys
|
|
23944
|
+
if (this.canvasManager.hasSelectedElements()) {
|
|
22359
23945
|
const editingTextBox = this.canvasManager.getEditingTextBox();
|
|
22360
23946
|
const focusedTable = this.canvasManager.getFocusedControl();
|
|
22361
23947
|
const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
|
|
22362
23948
|
if (!isEditing) {
|
|
22363
|
-
//
|
|
22364
|
-
|
|
23949
|
+
// Arrow keys: deselect and move cursor in text flow
|
|
23950
|
+
const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
|
|
23951
|
+
if (isArrowKey) {
|
|
23952
|
+
this.canvasManager.clearSelection();
|
|
23953
|
+
// Fall through to normal key handling
|
|
23954
|
+
}
|
|
23955
|
+
// Backspace/Delete: delete the selected object from the text flow
|
|
23956
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
23957
|
+
e.preventDefault();
|
|
23958
|
+
this.deleteSelectedElements();
|
|
23959
|
+
return;
|
|
23960
|
+
}
|
|
22365
23961
|
}
|
|
22366
23962
|
}
|
|
22367
23963
|
// Use the unified focus system to get the currently focused control
|
|
@@ -22464,6 +24060,32 @@ class PCEditor extends EventEmitter {
|
|
|
22464
24060
|
this.canvasManager.clearSelection();
|
|
22465
24061
|
this.canvasManager.render();
|
|
22466
24062
|
}
|
|
24063
|
+
/**
|
|
24064
|
+
* Delete all currently selected embedded objects from the text flow.
|
|
24065
|
+
*/
|
|
24066
|
+
deleteSelectedElements() {
|
|
24067
|
+
const selectedElements = this.canvasManager.getSelectedElements();
|
|
24068
|
+
if (selectedElements.length === 0)
|
|
24069
|
+
return;
|
|
24070
|
+
for (const elementId of selectedElements) {
|
|
24071
|
+
const objectInfo = this.findEmbeddedObjectInfo(elementId);
|
|
24072
|
+
if (objectInfo) {
|
|
24073
|
+
// Delete the placeholder character at the object's text index
|
|
24074
|
+
// This removes the object from the text flow
|
|
24075
|
+
objectInfo.content.deleteText(objectInfo.textIndex, 1);
|
|
24076
|
+
// Return focus to the parent flowing content
|
|
24077
|
+
const cursorPos = Math.min(objectInfo.textIndex, objectInfo.content.getText().length);
|
|
24078
|
+
objectInfo.content.setCursorPosition(cursorPos);
|
|
24079
|
+
this.canvasManager.setFocus(objectInfo.content);
|
|
24080
|
+
if (objectInfo.section !== this.canvasManager.getActiveSection()) {
|
|
24081
|
+
this.canvasManager.setActiveSection(objectInfo.section);
|
|
24082
|
+
}
|
|
24083
|
+
}
|
|
24084
|
+
}
|
|
24085
|
+
this.canvasManager.clearSelection();
|
|
24086
|
+
this.canvasManager.render();
|
|
24087
|
+
this.emit('content-changed', {});
|
|
24088
|
+
}
|
|
22467
24089
|
/**
|
|
22468
24090
|
* Find embedded object info by ID across all flowing content sources.
|
|
22469
24091
|
*/
|
|
@@ -23581,11 +25203,17 @@ class PCEditor extends EventEmitter {
|
|
|
23581
25203
|
let totalFieldCount = 0;
|
|
23582
25204
|
// Step 1: Expand repeating sections in body (header/footer don't support them)
|
|
23583
25205
|
this.expandRepeatingSections(bodyContent, data);
|
|
23584
|
-
// Step 2:
|
|
25206
|
+
// Step 2: Evaluate conditional sections in body (remove content where predicate is false)
|
|
25207
|
+
this.evaluateConditionalSections(bodyContent, data);
|
|
25208
|
+
// Step 3: Expand table row loops in body, header, and footer
|
|
23585
25209
|
this.expandTableRowLoops(bodyContent, data);
|
|
23586
25210
|
this.expandTableRowLoops(this.document.headerFlowingContent, data);
|
|
23587
25211
|
this.expandTableRowLoops(this.document.footerFlowingContent, data);
|
|
23588
|
-
// Step
|
|
25212
|
+
// Step 4: Evaluate table row conditionals in body, header, and footer
|
|
25213
|
+
this.evaluateTableRowConditionals(bodyContent, data);
|
|
25214
|
+
this.evaluateTableRowConditionals(this.document.headerFlowingContent, data);
|
|
25215
|
+
this.evaluateTableRowConditionals(this.document.footerFlowingContent, data);
|
|
25216
|
+
// Step 5: Substitute all fields in body
|
|
23589
25217
|
totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
|
|
23590
25218
|
// Step 4: Substitute all fields in embedded objects in body
|
|
23591
25219
|
totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
|
|
@@ -23788,14 +25416,67 @@ class PCEditor extends EventEmitter {
|
|
|
23788
25416
|
}
|
|
23789
25417
|
}
|
|
23790
25418
|
}
|
|
23791
|
-
// Rewrite field names in the original (first) iteration to use index 0
|
|
23792
|
-
for (const field of fieldsInSection) {
|
|
23793
|
-
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
23794
|
-
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
25419
|
+
// Rewrite field names in the original (first) iteration to use index 0
|
|
25420
|
+
for (const field of fieldsInSection) {
|
|
25421
|
+
const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
|
|
25422
|
+
fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
|
|
25423
|
+
}
|
|
25424
|
+
// Remove the section after expansion
|
|
25425
|
+
sectionManager.remove(section.id);
|
|
25426
|
+
}
|
|
25427
|
+
}
|
|
25428
|
+
/**
|
|
25429
|
+
* Evaluate conditional sections by removing content where predicate is false.
|
|
25430
|
+
* Processes sections from end to start to preserve text indices.
|
|
25431
|
+
*/
|
|
25432
|
+
evaluateConditionalSections(flowingContent, data) {
|
|
25433
|
+
const sectionManager = flowingContent.getConditionalSectionManager();
|
|
25434
|
+
// Get sections in descending order (process end-to-start)
|
|
25435
|
+
const sections = sectionManager.getSectionsDescending();
|
|
25436
|
+
for (const section of sections) {
|
|
25437
|
+
const result = PredicateEvaluator.evaluate(section.predicate, data);
|
|
25438
|
+
if (!result) {
|
|
25439
|
+
// Predicate is false — remove the content within this section
|
|
25440
|
+
const deleteStart = section.startIndex;
|
|
25441
|
+
const deleteLength = section.endIndex - section.startIndex;
|
|
25442
|
+
flowingContent.deleteText(deleteStart, deleteLength);
|
|
25443
|
+
}
|
|
25444
|
+
// Remove the conditional section marker regardless
|
|
25445
|
+
sectionManager.remove(section.id);
|
|
25446
|
+
}
|
|
25447
|
+
}
|
|
25448
|
+
/**
|
|
25449
|
+
* Evaluate table row conditionals in embedded tables within a FlowingTextContent.
|
|
25450
|
+
* For each table with row conditionals, removes rows where predicate is false.
|
|
25451
|
+
*/
|
|
25452
|
+
evaluateTableRowConditionals(flowingContent, data) {
|
|
25453
|
+
const embeddedObjects = flowingContent.getEmbeddedObjects();
|
|
25454
|
+
for (const [, obj] of embeddedObjects.entries()) {
|
|
25455
|
+
if (obj instanceof TableObject) {
|
|
25456
|
+
this.evaluateTableRowConditionalsInTable(obj, data);
|
|
25457
|
+
}
|
|
25458
|
+
}
|
|
25459
|
+
}
|
|
25460
|
+
/**
|
|
25461
|
+
* Evaluate row conditionals in a single table.
|
|
25462
|
+
* Processes conditionals from end to start to preserve row indices.
|
|
25463
|
+
*/
|
|
25464
|
+
evaluateTableRowConditionalsInTable(table, data) {
|
|
25465
|
+
const conditionals = table.getAllRowConditionals();
|
|
25466
|
+
if (conditionals.length === 0)
|
|
25467
|
+
return;
|
|
25468
|
+
// Sort by startRowIndex descending (process end-to-start)
|
|
25469
|
+
const sorted = [...conditionals].sort((a, b) => b.startRowIndex - a.startRowIndex);
|
|
25470
|
+
for (const cond of sorted) {
|
|
25471
|
+
const result = PredicateEvaluator.evaluate(cond.predicate, data);
|
|
25472
|
+
if (!result) {
|
|
25473
|
+
// Predicate is false — remove the rows
|
|
25474
|
+
table.removeRowsInRange(cond.startRowIndex, cond.endRowIndex);
|
|
23795
25475
|
}
|
|
23796
|
-
// Remove the
|
|
23797
|
-
|
|
25476
|
+
// Remove the conditional marker regardless
|
|
25477
|
+
table.removeRowConditional(cond.id);
|
|
23798
25478
|
}
|
|
25479
|
+
table.markLayoutDirty();
|
|
23799
25480
|
}
|
|
23800
25481
|
/**
|
|
23801
25482
|
* Get a value at a path without array defaulting.
|
|
@@ -24044,7 +25725,7 @@ class PCEditor extends EventEmitter {
|
|
|
24044
25725
|
toggleBulletList() {
|
|
24045
25726
|
if (!this._isReady)
|
|
24046
25727
|
return;
|
|
24047
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25728
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24048
25729
|
if (!flowingContent)
|
|
24049
25730
|
return;
|
|
24050
25731
|
flowingContent.toggleBulletList();
|
|
@@ -24057,7 +25738,7 @@ class PCEditor extends EventEmitter {
|
|
|
24057
25738
|
toggleNumberedList() {
|
|
24058
25739
|
if (!this._isReady)
|
|
24059
25740
|
return;
|
|
24060
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25741
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24061
25742
|
if (!flowingContent)
|
|
24062
25743
|
return;
|
|
24063
25744
|
flowingContent.toggleNumberedList();
|
|
@@ -24070,7 +25751,7 @@ class PCEditor extends EventEmitter {
|
|
|
24070
25751
|
indentParagraph() {
|
|
24071
25752
|
if (!this._isReady)
|
|
24072
25753
|
return;
|
|
24073
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25754
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24074
25755
|
if (!flowingContent)
|
|
24075
25756
|
return;
|
|
24076
25757
|
flowingContent.indentParagraph();
|
|
@@ -24083,7 +25764,7 @@ class PCEditor extends EventEmitter {
|
|
|
24083
25764
|
outdentParagraph() {
|
|
24084
25765
|
if (!this._isReady)
|
|
24085
25766
|
return;
|
|
24086
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25767
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24087
25768
|
if (!flowingContent)
|
|
24088
25769
|
return;
|
|
24089
25770
|
flowingContent.outdentParagraph();
|
|
@@ -24096,7 +25777,7 @@ class PCEditor extends EventEmitter {
|
|
|
24096
25777
|
getListFormatting() {
|
|
24097
25778
|
if (!this._isReady)
|
|
24098
25779
|
return undefined;
|
|
24099
|
-
const flowingContent = this.getActiveFlowingContent();
|
|
25780
|
+
const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
|
|
24100
25781
|
if (!flowingContent)
|
|
24101
25782
|
return undefined;
|
|
24102
25783
|
return flowingContent.getListFormatting();
|
|
@@ -24307,9 +25988,12 @@ class PCEditor extends EventEmitter {
|
|
|
24307
25988
|
// If a table is focused, create a row loop instead of a text repeating section
|
|
24308
25989
|
const focusedTable = this.getFocusedTable();
|
|
24309
25990
|
if (focusedTable && focusedTable.focusedCell) {
|
|
24310
|
-
|
|
24311
|
-
const
|
|
24312
|
-
const
|
|
25991
|
+
// Use the selected range if multiple rows are selected, otherwise use the focused cell's row
|
|
25992
|
+
const selectedRange = focusedTable.selectedRange;
|
|
25993
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
25994
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
25995
|
+
Logger.log('[pc-editor] createRepeatingSection → table row loop', startRow, endRow, fieldPath);
|
|
25996
|
+
const loop = focusedTable.createRowLoop(startRow, endRow, fieldPath);
|
|
24313
25997
|
if (loop) {
|
|
24314
25998
|
this.canvasManager.render();
|
|
24315
25999
|
this.emit('table-row-loop-added', { table: focusedTable, loop });
|
|
@@ -24381,6 +26065,103 @@ class PCEditor extends EventEmitter {
|
|
|
24381
26065
|
return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
|
|
24382
26066
|
}
|
|
24383
26067
|
// ============================================
|
|
26068
|
+
// Conditional Section API
|
|
26069
|
+
// ============================================
|
|
26070
|
+
/**
|
|
26071
|
+
* Create a conditional section.
|
|
26072
|
+
*
|
|
26073
|
+
* If a table is currently being edited (focused), creates a table row conditional
|
|
26074
|
+
* based on the focused cell's row.
|
|
26075
|
+
*
|
|
26076
|
+
* Otherwise, creates a body text conditional section at the given paragraph boundaries.
|
|
26077
|
+
*
|
|
26078
|
+
* @param startIndex Text index at paragraph start (ignored for table row conditionals)
|
|
26079
|
+
* @param endIndex Text index at closing paragraph start (ignored for table row conditionals)
|
|
26080
|
+
* @param predicate The predicate expression to evaluate (e.g., "isActive")
|
|
26081
|
+
* @returns The created section, or null if creation failed
|
|
26082
|
+
*/
|
|
26083
|
+
addConditionalSection(startIndex, endIndex, predicate) {
|
|
26084
|
+
if (!this._isReady) {
|
|
26085
|
+
throw new Error('Editor is not ready');
|
|
26086
|
+
}
|
|
26087
|
+
// If a table is focused, create a row conditional instead
|
|
26088
|
+
const focusedTable = this.getFocusedTable();
|
|
26089
|
+
if (focusedTable && focusedTable.focusedCell) {
|
|
26090
|
+
const selectedRange = focusedTable.selectedRange;
|
|
26091
|
+
const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
|
|
26092
|
+
const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
|
|
26093
|
+
Logger.log('[pc-editor] addConditionalSection → table row conditional', startRow, endRow, predicate);
|
|
26094
|
+
const cond = focusedTable.createRowConditional(startRow, endRow, predicate);
|
|
26095
|
+
if (cond) {
|
|
26096
|
+
this.canvasManager.render();
|
|
26097
|
+
this.emit('table-row-conditional-added', { table: focusedTable, conditional: cond });
|
|
26098
|
+
}
|
|
26099
|
+
return null; // Row conditionals are not ConditionalSections, return null
|
|
26100
|
+
}
|
|
26101
|
+
Logger.log('[pc-editor] addConditionalSection', startIndex, endIndex, predicate);
|
|
26102
|
+
const section = this.document.bodyFlowingContent.createConditionalSection(startIndex, endIndex, predicate);
|
|
26103
|
+
if (section) {
|
|
26104
|
+
this.canvasManager.render();
|
|
26105
|
+
this.emit('conditional-section-added', { section });
|
|
26106
|
+
}
|
|
26107
|
+
return section;
|
|
26108
|
+
}
|
|
26109
|
+
/**
|
|
26110
|
+
* Get a conditional section by ID.
|
|
26111
|
+
*/
|
|
26112
|
+
getConditionalSection(id) {
|
|
26113
|
+
if (!this._isReady) {
|
|
26114
|
+
return null;
|
|
26115
|
+
}
|
|
26116
|
+
return this.document.bodyFlowingContent.getConditionalSection(id) || null;
|
|
26117
|
+
}
|
|
26118
|
+
/**
|
|
26119
|
+
* Get all conditional sections.
|
|
26120
|
+
*/
|
|
26121
|
+
getConditionalSections() {
|
|
26122
|
+
if (!this._isReady) {
|
|
26123
|
+
return [];
|
|
26124
|
+
}
|
|
26125
|
+
return this.document.bodyFlowingContent.getConditionalSections();
|
|
26126
|
+
}
|
|
26127
|
+
/**
|
|
26128
|
+
* Update a conditional section's predicate.
|
|
26129
|
+
*/
|
|
26130
|
+
updateConditionalSectionPredicate(id, predicate) {
|
|
26131
|
+
if (!this._isReady) {
|
|
26132
|
+
return false;
|
|
26133
|
+
}
|
|
26134
|
+
const success = this.document.bodyFlowingContent.updateConditionalSectionPredicate(id, predicate);
|
|
26135
|
+
if (success) {
|
|
26136
|
+
this.canvasManager.render();
|
|
26137
|
+
this.emit('conditional-section-updated', { id, predicate });
|
|
26138
|
+
}
|
|
26139
|
+
return success;
|
|
26140
|
+
}
|
|
26141
|
+
/**
|
|
26142
|
+
* Remove a conditional section by ID.
|
|
26143
|
+
*/
|
|
26144
|
+
removeConditionalSection(id) {
|
|
26145
|
+
if (!this._isReady) {
|
|
26146
|
+
return false;
|
|
26147
|
+
}
|
|
26148
|
+
const success = this.document.bodyFlowingContent.removeConditionalSection(id);
|
|
26149
|
+
if (success) {
|
|
26150
|
+
this.canvasManager.render();
|
|
26151
|
+
this.emit('conditional-section-removed', { id });
|
|
26152
|
+
}
|
|
26153
|
+
return success;
|
|
26154
|
+
}
|
|
26155
|
+
/**
|
|
26156
|
+
* Find a conditional section that has a boundary at the given text index.
|
|
26157
|
+
*/
|
|
26158
|
+
getConditionalSectionAtBoundary(textIndex) {
|
|
26159
|
+
if (!this._isReady) {
|
|
26160
|
+
return null;
|
|
26161
|
+
}
|
|
26162
|
+
return this.document.bodyFlowingContent.getConditionalSectionAtBoundary(textIndex) || null;
|
|
26163
|
+
}
|
|
26164
|
+
// ============================================
|
|
24384
26165
|
// Header/Footer API
|
|
24385
26166
|
// ============================================
|
|
24386
26167
|
/**
|
|
@@ -24731,6 +26512,39 @@ class PCEditor extends EventEmitter {
|
|
|
24731
26512
|
setLogging(enabled) {
|
|
24732
26513
|
Logger.setEnabled(enabled);
|
|
24733
26514
|
}
|
|
26515
|
+
// ============================================
|
|
26516
|
+
// Font Management
|
|
26517
|
+
// ============================================
|
|
26518
|
+
/**
|
|
26519
|
+
* Register a custom font for use in the editor and PDF export.
|
|
26520
|
+
* The font will be loaded via the FontFace API for canvas rendering
|
|
26521
|
+
* and its raw bytes stored for PDF embedding.
|
|
26522
|
+
* @param options Font registration options (family + url or data)
|
|
26523
|
+
*/
|
|
26524
|
+
async registerFont(options) {
|
|
26525
|
+
Logger.log('[pc-editor] registerFont', options.family);
|
|
26526
|
+
await this.fontManager.registerFont(options);
|
|
26527
|
+
this.emit('font-registered', { family: options.family });
|
|
26528
|
+
// Re-render to pick up the new font if it's already in use
|
|
26529
|
+
if (this._isReady) {
|
|
26530
|
+
this.canvasManager.render();
|
|
26531
|
+
}
|
|
26532
|
+
}
|
|
26533
|
+
/**
|
|
26534
|
+
* Get all registered fonts (built-in and custom).
|
|
26535
|
+
*/
|
|
26536
|
+
getAvailableFonts() {
|
|
26537
|
+
return this.fontManager.getAvailableFonts().map(f => ({
|
|
26538
|
+
family: f.family,
|
|
26539
|
+
source: f.source
|
|
26540
|
+
}));
|
|
26541
|
+
}
|
|
26542
|
+
/**
|
|
26543
|
+
* Get all available font family names.
|
|
26544
|
+
*/
|
|
26545
|
+
getAvailableFontFamilies() {
|
|
26546
|
+
return this.fontManager.getAvailableFontFamilies();
|
|
26547
|
+
}
|
|
24734
26548
|
destroy() {
|
|
24735
26549
|
this.disableTextInput();
|
|
24736
26550
|
if (this.canvasManager) {
|
|
@@ -26247,7 +28061,7 @@ class MergeDataPane extends BasePane {
|
|
|
26247
28061
|
createContent() {
|
|
26248
28062
|
const container = document.createElement('div');
|
|
26249
28063
|
// Textarea for JSON
|
|
26250
|
-
const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
|
|
28064
|
+
const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
|
|
26251
28065
|
container.appendChild(textareaGroup);
|
|
26252
28066
|
// Error hint (hidden by default)
|
|
26253
28067
|
this.errorHint = this.createHint('');
|
|
@@ -26393,17 +28207,29 @@ class FormattingPane extends BasePane {
|
|
|
26393
28207
|
attach(options) {
|
|
26394
28208
|
super.attach(options);
|
|
26395
28209
|
if (this.editor) {
|
|
28210
|
+
// Populate font list from editor if no explicit list was provided
|
|
28211
|
+
if (this.fontFamilies === DEFAULT_FONT_FAMILIES) {
|
|
28212
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28213
|
+
this.rebuildFontSelect();
|
|
28214
|
+
}
|
|
26396
28215
|
// Update on cursor/selection changes
|
|
26397
28216
|
const updateHandler = () => this.updateFromEditor();
|
|
26398
28217
|
this.editor.on('cursor-changed', updateHandler);
|
|
26399
28218
|
this.editor.on('selection-changed', updateHandler);
|
|
26400
28219
|
this.editor.on('text-changed', updateHandler);
|
|
26401
28220
|
this.editor.on('formatting-changed', updateHandler);
|
|
28221
|
+
// Update font list when new fonts are registered
|
|
28222
|
+
const fontHandler = () => {
|
|
28223
|
+
this.fontFamilies = this.editor.getAvailableFontFamilies();
|
|
28224
|
+
this.rebuildFontSelect();
|
|
28225
|
+
};
|
|
28226
|
+
this.editor.on('font-registered', fontHandler);
|
|
26402
28227
|
this.eventCleanup.push(() => {
|
|
26403
28228
|
this.editor?.off('cursor-changed', updateHandler);
|
|
26404
28229
|
this.editor?.off('selection-changed', updateHandler);
|
|
26405
28230
|
this.editor?.off('text-changed', updateHandler);
|
|
26406
28231
|
this.editor?.off('formatting-changed', updateHandler);
|
|
28232
|
+
this.editor?.off('font-registered', fontHandler);
|
|
26407
28233
|
});
|
|
26408
28234
|
// Initial update
|
|
26409
28235
|
this.updateFromEditor();
|
|
@@ -26472,38 +28298,82 @@ class FormattingPane extends BasePane {
|
|
|
26472
28298
|
listsGroup.appendChild(this.outdentBtn);
|
|
26473
28299
|
listsSection.appendChild(listsGroup);
|
|
26474
28300
|
container.appendChild(listsSection);
|
|
26475
|
-
// Font section
|
|
28301
|
+
// Font section - label-value grid with right-aligned labels
|
|
26476
28302
|
const fontSection = this.createSection('Font');
|
|
28303
|
+
const fontGrid = document.createElement('div');
|
|
28304
|
+
fontGrid.className = 'pc-pane-label-value-grid';
|
|
28305
|
+
// Family row
|
|
28306
|
+
const familyLabel = document.createElement('label');
|
|
28307
|
+
familyLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28308
|
+
familyLabel.textContent = 'Family:';
|
|
26477
28309
|
this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
|
|
26478
28310
|
this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
|
|
26479
|
-
|
|
28311
|
+
fontGrid.appendChild(familyLabel);
|
|
28312
|
+
fontGrid.appendChild(this.fontFamilySelect);
|
|
28313
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28314
|
+
// Size row
|
|
28315
|
+
const sizeLabel = document.createElement('label');
|
|
28316
|
+
sizeLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28317
|
+
sizeLabel.textContent = 'Size:';
|
|
26480
28318
|
this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
|
|
26481
28319
|
this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
|
|
26482
|
-
|
|
28320
|
+
fontGrid.appendChild(sizeLabel);
|
|
28321
|
+
fontGrid.appendChild(this.fontSizeSelect);
|
|
28322
|
+
fontGrid.appendChild(document.createElement('div'));
|
|
28323
|
+
fontSection.appendChild(fontGrid);
|
|
26483
28324
|
container.appendChild(fontSection);
|
|
26484
|
-
// Color section
|
|
28325
|
+
// Color section - label-value grid with right-aligned labels
|
|
26485
28326
|
const colorSection = this.createSection('Color');
|
|
26486
|
-
const
|
|
26487
|
-
|
|
28327
|
+
const colorGrid = document.createElement('div');
|
|
28328
|
+
colorGrid.className = 'pc-pane-label-value-grid';
|
|
28329
|
+
// Text color row: label | picker | spacer
|
|
28330
|
+
const textColorLabel = document.createElement('label');
|
|
28331
|
+
textColorLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28332
|
+
textColorLabel.textContent = 'Text:';
|
|
26488
28333
|
this.colorInput = this.createColorInput('#000000');
|
|
26489
28334
|
this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
|
|
28335
|
+
colorGrid.appendChild(textColorLabel);
|
|
28336
|
+
colorGrid.appendChild(this.colorInput);
|
|
28337
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28338
|
+
// Highlight row: label | picker + clear button | spacer
|
|
28339
|
+
const highlightLabel = document.createElement('label');
|
|
28340
|
+
highlightLabel.className = 'pc-pane-label pc-pane-margin-label';
|
|
28341
|
+
highlightLabel.textContent = 'Highlight:';
|
|
26493
28342
|
this.highlightInput = this.createColorInput('#ffff00');
|
|
26494
28343
|
this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
|
|
26495
|
-
const
|
|
28344
|
+
const highlightControls = document.createElement('div');
|
|
28345
|
+
highlightControls.style.display = 'flex';
|
|
28346
|
+
highlightControls.style.alignItems = 'center';
|
|
28347
|
+
highlightControls.style.gap = '4px';
|
|
28348
|
+
highlightControls.appendChild(this.highlightInput);
|
|
26496
28349
|
const clearHighlightBtn = this.createButton('Clear');
|
|
26497
28350
|
clearHighlightBtn.className = 'pc-pane-button';
|
|
26498
|
-
clearHighlightBtn.style.marginLeft = '4px';
|
|
26499
28351
|
this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
28352
|
+
highlightControls.appendChild(clearHighlightBtn);
|
|
28353
|
+
colorGrid.appendChild(highlightLabel);
|
|
28354
|
+
colorGrid.appendChild(highlightControls);
|
|
28355
|
+
colorGrid.appendChild(document.createElement('div'));
|
|
28356
|
+
colorSection.appendChild(colorGrid);
|
|
26504
28357
|
container.appendChild(colorSection);
|
|
26505
28358
|
return container;
|
|
26506
28359
|
}
|
|
28360
|
+
rebuildFontSelect() {
|
|
28361
|
+
if (!this.fontFamilySelect)
|
|
28362
|
+
return;
|
|
28363
|
+
const currentValue = this.fontFamilySelect.value;
|
|
28364
|
+
this.fontFamilySelect.innerHTML = '';
|
|
28365
|
+
for (const family of this.fontFamilies) {
|
|
28366
|
+
const option = document.createElement('option');
|
|
28367
|
+
option.value = family;
|
|
28368
|
+
option.textContent = family;
|
|
28369
|
+
option.style.fontFamily = family;
|
|
28370
|
+
this.fontFamilySelect.appendChild(option);
|
|
28371
|
+
}
|
|
28372
|
+
// Restore selection if the font still exists
|
|
28373
|
+
if (this.fontFamilies.includes(currentValue)) {
|
|
28374
|
+
this.fontFamilySelect.value = currentValue;
|
|
28375
|
+
}
|
|
28376
|
+
}
|
|
26507
28377
|
updateFromEditor() {
|
|
26508
28378
|
if (!this.editor)
|
|
26509
28379
|
return;
|
|
@@ -26727,10 +28597,10 @@ class HyperlinkPane extends BasePane {
|
|
|
26727
28597
|
const container = document.createElement('div');
|
|
26728
28598
|
// URL input
|
|
26729
28599
|
this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
|
|
26730
|
-
container.appendChild(this.createFormGroup('URL', this.urlInput));
|
|
28600
|
+
container.appendChild(this.createFormGroup('URL:', this.urlInput));
|
|
26731
28601
|
// Title input
|
|
26732
28602
|
this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
|
|
26733
|
-
container.appendChild(this.createFormGroup('Title', this.titleInput));
|
|
28603
|
+
container.appendChild(this.createFormGroup('Title:', this.titleInput));
|
|
26734
28604
|
// Apply button
|
|
26735
28605
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
26736
28606
|
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
@@ -26881,10 +28751,10 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26881
28751
|
const container = document.createElement('div');
|
|
26882
28752
|
// Field name input
|
|
26883
28753
|
this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
|
|
26884
|
-
container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
|
|
28754
|
+
container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
|
|
26885
28755
|
// Default value input
|
|
26886
28756
|
this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
|
|
26887
|
-
container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
|
|
28757
|
+
container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
|
|
26888
28758
|
// Value type select
|
|
26889
28759
|
this.valueTypeSelect = this.createSelect([
|
|
26890
28760
|
{ value: '', label: '(None)' },
|
|
@@ -26893,7 +28763,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26893
28763
|
{ value: 'date', label: 'Date' }
|
|
26894
28764
|
]);
|
|
26895
28765
|
this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
|
|
26896
|
-
container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
|
|
28766
|
+
container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
|
|
26897
28767
|
// Number format group
|
|
26898
28768
|
this.numberFormatGroup = this.createSection();
|
|
26899
28769
|
this.numberFormatGroup.style.display = 'none';
|
|
@@ -26903,7 +28773,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26903
28773
|
{ value: '0,0', label: 'Thousands separator (0,0)' },
|
|
26904
28774
|
{ value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
|
|
26905
28775
|
]);
|
|
26906
|
-
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
|
|
28776
|
+
this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
|
|
26907
28777
|
container.appendChild(this.numberFormatGroup);
|
|
26908
28778
|
// Currency format group
|
|
26909
28779
|
this.currencyFormatGroup = this.createSection();
|
|
@@ -26914,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26914
28784
|
{ value: 'GBP', label: 'GBP' },
|
|
26915
28785
|
{ value: 'JPY', label: 'JPY' }
|
|
26916
28786
|
]);
|
|
26917
|
-
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
|
|
28787
|
+
this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
|
|
26918
28788
|
container.appendChild(this.currencyFormatGroup);
|
|
26919
28789
|
// Date format group
|
|
26920
28790
|
this.dateFormatGroup = this.createSection();
|
|
@@ -26925,7 +28795,7 @@ class SubstitutionFieldPane extends BasePane {
|
|
|
26925
28795
|
{ value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
|
|
26926
28796
|
{ value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
|
|
26927
28797
|
]);
|
|
26928
|
-
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
|
|
28798
|
+
this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
|
|
26929
28799
|
container.appendChild(this.dateFormatGroup);
|
|
26930
28800
|
// Apply button
|
|
26931
28801
|
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
@@ -27087,12 +28957,17 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27087
28957
|
if (this.editor) {
|
|
27088
28958
|
// Listen for repeating section selection
|
|
27089
28959
|
const selectionHandler = (event) => {
|
|
27090
|
-
|
|
27091
|
-
|
|
28960
|
+
const sel = event.selection || event;
|
|
28961
|
+
if (sel.type === 'repeating-section' && sel.sectionId) {
|
|
28962
|
+
const section = this.editor?.getRepeatingSection(sel.sectionId);
|
|
27092
28963
|
if (section) {
|
|
27093
28964
|
this.showSection(section);
|
|
27094
28965
|
}
|
|
27095
28966
|
}
|
|
28967
|
+
else {
|
|
28968
|
+
// Selection changed away from repeating section — hide pane
|
|
28969
|
+
this.hideSection();
|
|
28970
|
+
}
|
|
27096
28971
|
};
|
|
27097
28972
|
const removedHandler = () => {
|
|
27098
28973
|
this.hideSection();
|
|
@@ -27109,7 +28984,7 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27109
28984
|
const container = document.createElement('div');
|
|
27110
28985
|
// Field path input
|
|
27111
28986
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27112
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
28987
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27113
28988
|
hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
|
|
27114
28989
|
}));
|
|
27115
28990
|
// Apply button
|
|
@@ -27209,6 +29084,158 @@ class RepeatingSectionPane extends BasePane {
|
|
|
27209
29084
|
}
|
|
27210
29085
|
}
|
|
27211
29086
|
|
|
29087
|
+
/**
|
|
29088
|
+
* ConditionalSectionPane - Edit conditional section properties.
|
|
29089
|
+
*
|
|
29090
|
+
* Shows:
|
|
29091
|
+
* - Predicate input (boolean expression in merge data)
|
|
29092
|
+
* - Position information
|
|
29093
|
+
*
|
|
29094
|
+
* Uses the PCEditor public API:
|
|
29095
|
+
* - editor.getConditionalSection()
|
|
29096
|
+
* - editor.updateConditionalSectionPredicate()
|
|
29097
|
+
* - editor.removeConditionalSection()
|
|
29098
|
+
*/
|
|
29099
|
+
class ConditionalSectionPane extends BasePane {
|
|
29100
|
+
constructor(id = 'conditional-section', options = {}) {
|
|
29101
|
+
super(id, { className: 'pc-pane-conditional-section', ...options });
|
|
29102
|
+
this.predicateInput = null;
|
|
29103
|
+
this.positionHint = null;
|
|
29104
|
+
this.currentSection = null;
|
|
29105
|
+
this.onApplyCallback = options.onApply;
|
|
29106
|
+
this.onRemoveCallback = options.onRemove;
|
|
29107
|
+
}
|
|
29108
|
+
attach(options) {
|
|
29109
|
+
super.attach(options);
|
|
29110
|
+
if (this.editor) {
|
|
29111
|
+
// Listen for conditional section selection
|
|
29112
|
+
const selectionHandler = (event) => {
|
|
29113
|
+
const sel = event.selection || event;
|
|
29114
|
+
if (sel.type === 'conditional-section' && sel.sectionId) {
|
|
29115
|
+
const section = this.editor?.getConditionalSection(sel.sectionId);
|
|
29116
|
+
if (section) {
|
|
29117
|
+
this.showSection(section);
|
|
29118
|
+
}
|
|
29119
|
+
}
|
|
29120
|
+
else {
|
|
29121
|
+
// Selection changed away from conditional section — hide pane
|
|
29122
|
+
this.hideSection();
|
|
29123
|
+
}
|
|
29124
|
+
};
|
|
29125
|
+
const removedHandler = () => {
|
|
29126
|
+
this.hideSection();
|
|
29127
|
+
};
|
|
29128
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29129
|
+
this.editor.on('conditional-section-removed', removedHandler);
|
|
29130
|
+
this.eventCleanup.push(() => {
|
|
29131
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29132
|
+
this.editor?.off('conditional-section-removed', removedHandler);
|
|
29133
|
+
});
|
|
29134
|
+
}
|
|
29135
|
+
}
|
|
29136
|
+
createContent() {
|
|
29137
|
+
const container = document.createElement('div');
|
|
29138
|
+
// Predicate input
|
|
29139
|
+
this.predicateInput = this.createTextInput({ placeholder: 'isActive' });
|
|
29140
|
+
container.appendChild(this.createFormGroup('Condition:', this.predicateInput, {
|
|
29141
|
+
hint: 'Boolean expression evaluated against merge data (e.g., "isActive", "count > 0")'
|
|
29142
|
+
}));
|
|
29143
|
+
// Apply button
|
|
29144
|
+
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
29145
|
+
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
29146
|
+
container.appendChild(applyBtn);
|
|
29147
|
+
// Remove button
|
|
29148
|
+
const removeBtn = this.createButton('Remove Condition', { variant: 'danger' });
|
|
29149
|
+
removeBtn.style.marginTop = '0.5rem';
|
|
29150
|
+
this.addButtonListener(removeBtn, () => this.removeSection());
|
|
29151
|
+
container.appendChild(removeBtn);
|
|
29152
|
+
// Position hint
|
|
29153
|
+
this.positionHint = this.createHint('');
|
|
29154
|
+
container.appendChild(this.positionHint);
|
|
29155
|
+
return container;
|
|
29156
|
+
}
|
|
29157
|
+
/**
|
|
29158
|
+
* Show the pane with the given section.
|
|
29159
|
+
*/
|
|
29160
|
+
showSection(section) {
|
|
29161
|
+
this.currentSection = section;
|
|
29162
|
+
if (this.predicateInput) {
|
|
29163
|
+
this.predicateInput.value = section.predicate;
|
|
29164
|
+
}
|
|
29165
|
+
if (this.positionHint) {
|
|
29166
|
+
this.positionHint.textContent = `Condition from position ${section.startIndex} to ${section.endIndex}`;
|
|
29167
|
+
}
|
|
29168
|
+
this.show();
|
|
29169
|
+
}
|
|
29170
|
+
/**
|
|
29171
|
+
* Hide the pane and clear the current section.
|
|
29172
|
+
*/
|
|
29173
|
+
hideSection() {
|
|
29174
|
+
this.currentSection = null;
|
|
29175
|
+
this.hide();
|
|
29176
|
+
}
|
|
29177
|
+
applyChanges() {
|
|
29178
|
+
if (!this.editor || !this.currentSection) {
|
|
29179
|
+
this.onApplyCallback?.(false, new Error('No section selected'));
|
|
29180
|
+
return;
|
|
29181
|
+
}
|
|
29182
|
+
const predicate = this.predicateInput?.value.trim();
|
|
29183
|
+
if (!predicate) {
|
|
29184
|
+
this.onApplyCallback?.(false, new Error('Predicate cannot be empty'));
|
|
29185
|
+
return;
|
|
29186
|
+
}
|
|
29187
|
+
if (predicate === this.currentSection.predicate) {
|
|
29188
|
+
return; // No changes
|
|
29189
|
+
}
|
|
29190
|
+
try {
|
|
29191
|
+
const success = this.editor.updateConditionalSectionPredicate(this.currentSection.id, predicate);
|
|
29192
|
+
if (success) {
|
|
29193
|
+
this.currentSection = this.editor.getConditionalSection(this.currentSection.id) || null;
|
|
29194
|
+
if (this.currentSection) {
|
|
29195
|
+
this.showSection(this.currentSection);
|
|
29196
|
+
}
|
|
29197
|
+
this.onApplyCallback?.(true);
|
|
29198
|
+
}
|
|
29199
|
+
else {
|
|
29200
|
+
this.onApplyCallback?.(false, new Error('Failed to update section'));
|
|
29201
|
+
}
|
|
29202
|
+
}
|
|
29203
|
+
catch (error) {
|
|
29204
|
+
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
29205
|
+
}
|
|
29206
|
+
}
|
|
29207
|
+
removeSection() {
|
|
29208
|
+
if (!this.editor || !this.currentSection)
|
|
29209
|
+
return;
|
|
29210
|
+
try {
|
|
29211
|
+
this.editor.removeConditionalSection(this.currentSection.id);
|
|
29212
|
+
this.hideSection();
|
|
29213
|
+
this.onRemoveCallback?.(true);
|
|
29214
|
+
}
|
|
29215
|
+
catch {
|
|
29216
|
+
this.onRemoveCallback?.(false);
|
|
29217
|
+
}
|
|
29218
|
+
}
|
|
29219
|
+
/**
|
|
29220
|
+
* Get the currently selected section.
|
|
29221
|
+
*/
|
|
29222
|
+
getCurrentSection() {
|
|
29223
|
+
return this.currentSection;
|
|
29224
|
+
}
|
|
29225
|
+
/**
|
|
29226
|
+
* Check if a section is currently selected.
|
|
29227
|
+
*/
|
|
29228
|
+
hasSection() {
|
|
29229
|
+
return this.currentSection !== null;
|
|
29230
|
+
}
|
|
29231
|
+
/**
|
|
29232
|
+
* Update the pane from current editor state.
|
|
29233
|
+
*/
|
|
29234
|
+
update() {
|
|
29235
|
+
// Section pane doesn't auto-update - it's driven by selection events
|
|
29236
|
+
}
|
|
29237
|
+
}
|
|
29238
|
+
|
|
27212
29239
|
/**
|
|
27213
29240
|
* TableRowLoopPane - Edit table row loop properties.
|
|
27214
29241
|
*
|
|
@@ -27233,14 +29260,28 @@ class TableRowLoopPane extends BasePane {
|
|
|
27233
29260
|
}
|
|
27234
29261
|
attach(options) {
|
|
27235
29262
|
super.attach(options);
|
|
27236
|
-
|
|
27237
|
-
|
|
29263
|
+
if (this.editor) {
|
|
29264
|
+
// Auto-show when a table row loop is clicked
|
|
29265
|
+
const loopClickHandler = (data) => {
|
|
29266
|
+
this.showLoop(data.table, data.loop);
|
|
29267
|
+
};
|
|
29268
|
+
// Hide when selection changes away from a loop
|
|
29269
|
+
const selectionHandler = () => {
|
|
29270
|
+
this.hideLoop();
|
|
29271
|
+
};
|
|
29272
|
+
this.editor.on('table-row-loop-clicked', loopClickHandler);
|
|
29273
|
+
this.editor.on('selection-change', selectionHandler);
|
|
29274
|
+
this.eventCleanup.push(() => {
|
|
29275
|
+
this.editor?.off('table-row-loop-clicked', loopClickHandler);
|
|
29276
|
+
this.editor?.off('selection-change', selectionHandler);
|
|
29277
|
+
});
|
|
29278
|
+
}
|
|
27238
29279
|
}
|
|
27239
29280
|
createContent() {
|
|
27240
29281
|
const container = document.createElement('div');
|
|
27241
29282
|
// Field path input
|
|
27242
29283
|
this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
|
|
27243
|
-
container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
|
|
29284
|
+
container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
|
|
27244
29285
|
hint: 'Path to array in merge data (e.g., "items" or "orders")'
|
|
27245
29286
|
}));
|
|
27246
29287
|
// Apply button
|
|
@@ -27400,56 +29441,63 @@ class TextBoxPane extends BasePane {
|
|
|
27400
29441
|
}
|
|
27401
29442
|
createContent() {
|
|
27402
29443
|
const container = document.createElement('div');
|
|
27403
|
-
// Position section
|
|
29444
|
+
// Position section - Type on same row as label
|
|
27404
29445
|
const positionSection = this.createSection('Position');
|
|
27405
29446
|
this.positionSelect = this.createSelect([
|
|
27406
29447
|
{ value: 'inline', label: 'Inline' },
|
|
27407
29448
|
{ value: 'block', label: 'Block' },
|
|
27408
29449
|
{ value: 'relative', label: 'Relative' }
|
|
27409
29450
|
], 'inline');
|
|
27410
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27411
|
-
|
|
29451
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29452
|
+
this.updateOffsetVisibility();
|
|
29453
|
+
this.applyChanges();
|
|
29454
|
+
});
|
|
29455
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27412
29456
|
// Offset group (only visible for relative positioning)
|
|
27413
29457
|
this.offsetGroup = document.createElement('div');
|
|
27414
29458
|
this.offsetGroup.style.display = 'none';
|
|
27415
29459
|
const offsetRow = this.createRow();
|
|
27416
29460
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27417
29461
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27418
|
-
|
|
27419
|
-
|
|
29462
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29463
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29464
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29465
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27420
29466
|
this.offsetGroup.appendChild(offsetRow);
|
|
27421
29467
|
positionSection.appendChild(this.offsetGroup);
|
|
27422
29468
|
container.appendChild(positionSection);
|
|
27423
|
-
// Background
|
|
27424
|
-
const bgSection = this.createSection(
|
|
29469
|
+
// Background - color on same row as label
|
|
29470
|
+
const bgSection = this.createSection();
|
|
27425
29471
|
this.bgColorInput = this.createColorInput('#ffffff');
|
|
27426
|
-
|
|
29472
|
+
this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
|
|
29473
|
+
bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
|
|
27427
29474
|
container.appendChild(bgSection);
|
|
27428
29475
|
// Border section
|
|
27429
29476
|
const borderSection = this.createSection('Border');
|
|
27430
29477
|
const borderRow = this.createRow();
|
|
27431
29478
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
|
|
27432
29479
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
27433
|
-
|
|
27434
|
-
|
|
29480
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyChanges());
|
|
29481
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyChanges());
|
|
29482
|
+
borderRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
29483
|
+
borderRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
27435
29484
|
borderSection.appendChild(borderRow);
|
|
29485
|
+
// Border style on same row as label
|
|
27436
29486
|
this.borderStyleSelect = this.createSelect([
|
|
27437
29487
|
{ value: 'solid', label: 'Solid' },
|
|
27438
29488
|
{ value: 'dashed', label: 'Dashed' },
|
|
27439
29489
|
{ value: 'dotted', label: 'Dotted' },
|
|
27440
29490
|
{ value: 'none', label: 'None' }
|
|
27441
29491
|
], 'solid');
|
|
27442
|
-
|
|
29492
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
|
|
29493
|
+
borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
27443
29494
|
container.appendChild(borderSection);
|
|
27444
|
-
// Padding
|
|
27445
|
-
const paddingSection = this.createSection(
|
|
29495
|
+
// Padding on same row as label
|
|
29496
|
+
const paddingSection = this.createSection();
|
|
27446
29497
|
this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
|
|
27447
|
-
|
|
29498
|
+
this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
|
|
29499
|
+
paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
|
|
27448
29500
|
container.appendChild(paddingSection);
|
|
27449
|
-
// Apply button
|
|
27450
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27451
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27452
|
-
container.appendChild(applyBtn);
|
|
27453
29501
|
return container;
|
|
27454
29502
|
}
|
|
27455
29503
|
updateFromSelection() {
|
|
@@ -27525,7 +29573,6 @@ class TextBoxPane extends BasePane {
|
|
|
27525
29573
|
}
|
|
27526
29574
|
applyChanges() {
|
|
27527
29575
|
if (!this.editor || !this.currentTextBox) {
|
|
27528
|
-
this.onApplyCallback?.(false, new Error('No text box selected'));
|
|
27529
29576
|
return;
|
|
27530
29577
|
}
|
|
27531
29578
|
const updates = {};
|
|
@@ -27561,12 +29608,7 @@ class TextBoxPane extends BasePane {
|
|
|
27561
29608
|
}
|
|
27562
29609
|
try {
|
|
27563
29610
|
const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
|
|
27564
|
-
|
|
27565
|
-
this.onApplyCallback?.(true);
|
|
27566
|
-
}
|
|
27567
|
-
else {
|
|
27568
|
-
this.onApplyCallback?.(false, new Error('Failed to update text box'));
|
|
27569
|
-
}
|
|
29611
|
+
this.onApplyCallback?.(success);
|
|
27570
29612
|
}
|
|
27571
29613
|
catch (error) {
|
|
27572
29614
|
this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -27642,28 +29684,35 @@ class ImagePane extends BasePane {
|
|
|
27642
29684
|
}
|
|
27643
29685
|
createContent() {
|
|
27644
29686
|
const container = document.createElement('div');
|
|
27645
|
-
// Position section
|
|
29687
|
+
// Position section — with heading, matching TextBoxPane
|
|
27646
29688
|
const positionSection = this.createSection('Position');
|
|
27647
29689
|
this.positionSelect = this.createSelect([
|
|
27648
29690
|
{ value: 'inline', label: 'Inline' },
|
|
27649
29691
|
{ value: 'block', label: 'Block' },
|
|
27650
29692
|
{ value: 'relative', label: 'Relative' }
|
|
27651
29693
|
], 'inline');
|
|
27652
|
-
this.addImmediateApplyListener(this.positionSelect, () =>
|
|
27653
|
-
|
|
29694
|
+
this.addImmediateApplyListener(this.positionSelect, () => {
|
|
29695
|
+
this.updateOffsetVisibility();
|
|
29696
|
+
this.applyChanges();
|
|
29697
|
+
});
|
|
29698
|
+
positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
|
|
27654
29699
|
// Offset group (only visible for relative positioning)
|
|
27655
29700
|
this.offsetGroup = document.createElement('div');
|
|
27656
29701
|
this.offsetGroup.style.display = 'none';
|
|
27657
29702
|
const offsetRow = this.createRow();
|
|
27658
29703
|
this.offsetXInput = this.createNumberInput({ value: 0 });
|
|
27659
29704
|
this.offsetYInput = this.createNumberInput({ value: 0 });
|
|
27660
|
-
|
|
27661
|
-
|
|
29705
|
+
this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
|
|
29706
|
+
this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
|
|
29707
|
+
offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
|
|
29708
|
+
offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
|
|
27662
29709
|
this.offsetGroup.appendChild(offsetRow);
|
|
27663
29710
|
positionSection.appendChild(this.offsetGroup);
|
|
27664
29711
|
container.appendChild(positionSection);
|
|
27665
|
-
|
|
27666
|
-
|
|
29712
|
+
container.appendChild(document.createElement('hr'));
|
|
29713
|
+
// Display section — Fit Mode and Resize Mode with aligned labels
|
|
29714
|
+
const displaySection = document.createElement('div');
|
|
29715
|
+
displaySection.className = 'pc-pane-image-display';
|
|
27667
29716
|
this.fitModeSelect = this.createSelect([
|
|
27668
29717
|
{ value: 'contain', label: 'Contain' },
|
|
27669
29718
|
{ value: 'cover', label: 'Cover' },
|
|
@@ -27671,34 +29720,31 @@ class ImagePane extends BasePane {
|
|
|
27671
29720
|
{ value: 'none', label: 'None (original size)' },
|
|
27672
29721
|
{ value: 'tile', label: 'Tile' }
|
|
27673
29722
|
], 'contain');
|
|
27674
|
-
|
|
29723
|
+
this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
|
|
29724
|
+
displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
|
|
27675
29725
|
this.resizeModeSelect = this.createSelect([
|
|
27676
29726
|
{ value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
|
|
27677
29727
|
{ value: 'free', label: 'Free Resize' }
|
|
27678
29728
|
], 'locked-aspect-ratio');
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
29729
|
+
this.addImmediateApplyListener(this.resizeModeSelect, () => this.applyChanges());
|
|
29730
|
+
displaySection.appendChild(this.createFormGroup('Resize Mode:', this.resizeModeSelect, { inline: true }));
|
|
29731
|
+
container.appendChild(displaySection);
|
|
29732
|
+
container.appendChild(document.createElement('hr'));
|
|
29733
|
+
// Alt Text — inline row
|
|
27683
29734
|
this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
|
|
27684
|
-
|
|
27685
|
-
container.appendChild(
|
|
27686
|
-
|
|
27687
|
-
|
|
29735
|
+
this.addImmediateApplyListener(this.altTextInput, () => this.applyChanges());
|
|
29736
|
+
container.appendChild(this.createFormGroup('Alt Text:', this.altTextInput, { inline: true }));
|
|
29737
|
+
container.appendChild(document.createElement('hr'));
|
|
29738
|
+
// Source — change image button
|
|
27688
29739
|
this.fileInput = document.createElement('input');
|
|
27689
29740
|
this.fileInput.type = 'file';
|
|
27690
29741
|
this.fileInput.accept = 'image/*';
|
|
27691
29742
|
this.fileInput.style.display = 'none';
|
|
27692
29743
|
this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
|
|
27693
|
-
|
|
29744
|
+
container.appendChild(this.fileInput);
|
|
27694
29745
|
const changeSourceBtn = this.createButton('Change Image...');
|
|
27695
29746
|
this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
|
|
27696
|
-
|
|
27697
|
-
container.appendChild(sourceSection);
|
|
27698
|
-
// Apply button
|
|
27699
|
-
const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
|
|
27700
|
-
this.addButtonListener(applyBtn, () => this.applyChanges());
|
|
27701
|
-
container.appendChild(applyBtn);
|
|
29747
|
+
container.appendChild(changeSourceBtn);
|
|
27702
29748
|
return container;
|
|
27703
29749
|
}
|
|
27704
29750
|
updateFromSelection() {
|
|
@@ -27917,60 +29963,57 @@ class TablePane extends BasePane {
|
|
|
27917
29963
|
const container = document.createElement('div');
|
|
27918
29964
|
// Structure section
|
|
27919
29965
|
const structureSection = this.createSection('Structure');
|
|
29966
|
+
// Rows/Columns info with aligned labels
|
|
27920
29967
|
const structureInfo = document.createElement('div');
|
|
27921
|
-
structureInfo.className = 'pc-pane-info
|
|
29968
|
+
structureInfo.className = 'pc-pane-table-structure-info';
|
|
27922
29969
|
this.rowCountDisplay = document.createElement('span');
|
|
29970
|
+
this.rowCountDisplay.className = 'pc-pane-info-value';
|
|
27923
29971
|
this.colCountDisplay = document.createElement('span');
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
rowInfo.appendChild(this.rowCountDisplay);
|
|
27928
|
-
const colInfo = document.createElement('div');
|
|
27929
|
-
colInfo.className = 'pc-pane-info';
|
|
27930
|
-
colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
|
|
27931
|
-
colInfo.appendChild(this.colCountDisplay);
|
|
27932
|
-
structureInfo.appendChild(rowInfo);
|
|
27933
|
-
structureInfo.appendChild(colInfo);
|
|
29972
|
+
this.colCountDisplay.className = 'pc-pane-info-value';
|
|
29973
|
+
structureInfo.appendChild(this.createFormGroup('Rows:', this.rowCountDisplay, { inline: true }));
|
|
29974
|
+
structureInfo.appendChild(this.createFormGroup('Columns:', this.colCountDisplay, { inline: true }));
|
|
27934
29975
|
structureSection.appendChild(structureInfo);
|
|
27935
|
-
// Row
|
|
27936
|
-
const
|
|
29976
|
+
// Row buttons
|
|
29977
|
+
const rowBtns = this.createButtonGroup();
|
|
27937
29978
|
const addRowBtn = this.createButton('+ Row');
|
|
27938
29979
|
this.addButtonListener(addRowBtn, () => this.insertRow());
|
|
27939
29980
|
const removeRowBtn = this.createButton('- Row');
|
|
27940
29981
|
this.addButtonListener(removeRowBtn, () => this.removeRow());
|
|
29982
|
+
rowBtns.appendChild(addRowBtn);
|
|
29983
|
+
rowBtns.appendChild(removeRowBtn);
|
|
29984
|
+
structureSection.appendChild(rowBtns);
|
|
29985
|
+
// Column buttons (separate row)
|
|
29986
|
+
const colBtns = this.createButtonGroup();
|
|
27941
29987
|
const addColBtn = this.createButton('+ Column');
|
|
27942
29988
|
this.addButtonListener(addColBtn, () => this.insertColumn());
|
|
27943
29989
|
const removeColBtn = this.createButton('- Column');
|
|
27944
29990
|
this.addButtonListener(removeColBtn, () => this.removeColumn());
|
|
27945
|
-
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
27949
|
-
structureSection.appendChild(
|
|
27950
|
-
|
|
27951
|
-
|
|
27952
|
-
const headersSection = this.createSection('Headers');
|
|
27953
|
-
const headerRow = this.createRow();
|
|
29991
|
+
colBtns.appendChild(addColBtn);
|
|
29992
|
+
colBtns.appendChild(removeColBtn);
|
|
29993
|
+
structureSection.appendChild(colBtns);
|
|
29994
|
+
// Header rows/cols (with separator and aligned labels)
|
|
29995
|
+
structureSection.appendChild(document.createElement('hr'));
|
|
29996
|
+
const headersGroup = document.createElement('div');
|
|
29997
|
+
headersGroup.className = 'pc-pane-table-headers';
|
|
27954
29998
|
this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
29999
|
+
this.addImmediateApplyListener(this.headerRowInput, () => this.applyHeaders());
|
|
30000
|
+
headersGroup.appendChild(this.createFormGroup('Header Rows:', this.headerRowInput, { inline: true }));
|
|
27955
30001
|
this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
|
|
27961
|
-
headersSection.appendChild(applyHeadersBtn);
|
|
27962
|
-
container.appendChild(headersSection);
|
|
27963
|
-
// Defaults section
|
|
30002
|
+
this.addImmediateApplyListener(this.headerColInput, () => this.applyHeaders());
|
|
30003
|
+
headersGroup.appendChild(this.createFormGroup('Header Cols:', this.headerColInput, { inline: true }));
|
|
30004
|
+
structureSection.appendChild(headersGroup);
|
|
30005
|
+
container.appendChild(structureSection);
|
|
30006
|
+
// Defaults section (aligned labels)
|
|
27964
30007
|
const defaultsSection = this.createSection('Defaults');
|
|
27965
|
-
const
|
|
30008
|
+
const defaultsGroup = document.createElement('div');
|
|
30009
|
+
defaultsGroup.className = 'pc-pane-table-defaults';
|
|
27966
30010
|
this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
|
|
30011
|
+
this.addImmediateApplyListener(this.defaultPaddingInput, () => this.applyDefaults());
|
|
30012
|
+
defaultsGroup.appendChild(this.createFormGroup('Padding:', this.defaultPaddingInput, { inline: true }));
|
|
27967
30013
|
this.defaultBorderColorInput = this.createColorInput('#cccccc');
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
defaultsSection.appendChild(
|
|
27971
|
-
const applyDefaultsBtn = this.createButton('Apply Defaults');
|
|
27972
|
-
this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
|
|
27973
|
-
defaultsSection.appendChild(applyDefaultsBtn);
|
|
30014
|
+
this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
|
|
30015
|
+
defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
|
|
30016
|
+
defaultsSection.appendChild(defaultsGroup);
|
|
27974
30017
|
container.appendChild(defaultsSection);
|
|
27975
30018
|
// Cell formatting section
|
|
27976
30019
|
const cellSection = this.createSection('Cell Formatting');
|
|
@@ -27987,9 +30030,11 @@ class TablePane extends BasePane {
|
|
|
27987
30030
|
mergeBtnGroup.appendChild(this.mergeCellsBtn);
|
|
27988
30031
|
mergeBtnGroup.appendChild(this.splitCellBtn);
|
|
27989
30032
|
cellSection.appendChild(mergeBtnGroup);
|
|
27990
|
-
|
|
30033
|
+
cellSection.appendChild(document.createElement('hr'));
|
|
30034
|
+
// Background — inline
|
|
27991
30035
|
this.cellBgColorInput = this.createColorInput('#ffffff');
|
|
27992
|
-
|
|
30036
|
+
this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
|
|
30037
|
+
cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
|
|
27993
30038
|
// Border checkboxes
|
|
27994
30039
|
const borderChecks = document.createElement('div');
|
|
27995
30040
|
borderChecks.className = 'pc-pane-row';
|
|
@@ -28021,24 +30066,29 @@ class TablePane extends BasePane {
|
|
|
28021
30066
|
checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
|
|
28022
30067
|
if (checkLabels[3])
|
|
28023
30068
|
checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
|
|
28024
|
-
|
|
30069
|
+
// Add change listeners for immediate apply on checkboxes
|
|
30070
|
+
for (const check of [this.borderTopCheck, this.borderRightCheck, this.borderBottomCheck, this.borderLeftCheck]) {
|
|
30071
|
+
check.addEventListener('change', () => this.applyCellFormatting());
|
|
30072
|
+
}
|
|
30073
|
+
cellSection.appendChild(this.createFormGroup('Borders:', borderChecks));
|
|
28025
30074
|
// Border properties
|
|
28026
30075
|
const borderPropsRow = this.createRow();
|
|
28027
30076
|
this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
|
|
28028
30077
|
this.borderColorInput = this.createColorInput('#cccccc');
|
|
28029
|
-
|
|
28030
|
-
|
|
30078
|
+
this.addImmediateApplyListener(this.borderWidthInput, () => this.applyCellFormatting());
|
|
30079
|
+
this.addImmediateApplyListener(this.borderColorInput, () => this.applyCellFormatting());
|
|
30080
|
+
borderPropsRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
|
|
30081
|
+
borderPropsRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
|
|
28031
30082
|
cellSection.appendChild(borderPropsRow);
|
|
30083
|
+
// Style — inline
|
|
28032
30084
|
this.borderStyleSelect = this.createSelect([
|
|
28033
30085
|
{ value: 'solid', label: 'Solid' },
|
|
28034
30086
|
{ value: 'dashed', label: 'Dashed' },
|
|
28035
30087
|
{ value: 'dotted', label: 'Dotted' },
|
|
28036
30088
|
{ value: 'none', label: 'None' }
|
|
28037
30089
|
], 'solid');
|
|
28038
|
-
|
|
28039
|
-
|
|
28040
|
-
this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
|
|
28041
|
-
cellSection.appendChild(applyCellBtn);
|
|
30090
|
+
this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
|
|
30091
|
+
cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
|
|
28042
30092
|
container.appendChild(cellSection);
|
|
28043
30093
|
return container;
|
|
28044
30094
|
}
|
|
@@ -28292,5 +30342,5 @@ class TablePane extends BasePane {
|
|
|
28292
30342
|
}
|
|
28293
30343
|
}
|
|
28294
30344
|
|
|
28295
|
-
export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, Logger, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
|
|
30345
|
+
export { BaseControl, BaseEmbeddedObject, BasePane, BaseTextRegion, BodyTextRegion, ClipboardManager, ConditionalSectionManager, ConditionalSectionPane, ContentAnalyzer, DEFAULT_IMPORT_OPTIONS, Document, DocumentBuilder, DocumentInfoPane, DocumentSettingsPane, EmbeddedObjectFactory, EmbeddedObjectManager, EventEmitter, FlowingTextContent, FontManager, FooterTextRegion, FormattingPane, HeaderTextRegion, HorizontalRuler, HtmlConverter, HyperlinkPane, ImageObject, ImagePane, Logger, MergeDataPane, PCEditor, PDFImportError, PDFImportErrorCode, PDFImporter, PDFParser, Page, PredicateEvaluator, RegionManager, RepeatingSectionManager, RepeatingSectionPane, RulerControl, SubstitutionFieldManager, SubstitutionFieldPane, TableCell, TableObject, TablePane, TableRow, TableRowLoopPane, TextBoxObject, TextBoxPane, TextFormattingManager, TextLayout, TextMeasurer, TextPositionCalculator, TextState, VerticalRuler, ViewSettingsPane };
|
|
28296
30346
|
//# sourceMappingURL=pc-editor.esm.js.map
|