@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.
Files changed (50) hide show
  1. package/dist/pc-editor.esm.js +2360 -310
  2. package/dist/pc-editor.esm.js.map +1 -1
  3. package/dist/pc-editor.js +2363 -309
  4. package/dist/pc-editor.js.map +1 -1
  5. package/dist/pc-editor.min.js +1 -1
  6. package/dist/pc-editor.min.js.map +1 -1
  7. package/dist/types/lib/core/PCEditor.d.ts +74 -1
  8. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  9. package/dist/types/lib/fonts/FontManager.d.ts +71 -0
  10. package/dist/types/lib/fonts/FontManager.d.ts.map +1 -0
  11. package/dist/types/lib/fonts/index.d.ts +3 -0
  12. package/dist/types/lib/fonts/index.d.ts.map +1 -0
  13. package/dist/types/lib/index.d.ts +6 -4
  14. package/dist/types/lib/index.d.ts.map +1 -1
  15. package/dist/types/lib/objects/table/TableObject.d.ts +72 -1
  16. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  17. package/dist/types/lib/objects/table/types.d.ts +20 -0
  18. package/dist/types/lib/objects/table/types.d.ts.map +1 -1
  19. package/dist/types/lib/panes/ConditionalSectionPane.d.ts +62 -0
  20. package/dist/types/lib/panes/ConditionalSectionPane.d.ts.map +1 -0
  21. package/dist/types/lib/panes/FormattingPane.d.ts +1 -0
  22. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -1
  23. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -1
  24. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -1
  25. package/dist/types/lib/panes/TablePane.d.ts.map +1 -1
  26. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -1
  27. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -1
  28. package/dist/types/lib/panes/index.d.ts +2 -0
  29. package/dist/types/lib/panes/index.d.ts.map +1 -1
  30. package/dist/types/lib/rendering/CanvasManager.d.ts +1 -0
  31. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  32. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +17 -1
  33. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  34. package/dist/types/lib/rendering/PDFGenerator.d.ts +13 -0
  35. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  36. package/dist/types/lib/text/ConditionalSectionManager.d.ts +101 -0
  37. package/dist/types/lib/text/ConditionalSectionManager.d.ts.map +1 -0
  38. package/dist/types/lib/text/FlowingTextContent.d.ts +44 -6
  39. package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
  40. package/dist/types/lib/text/ParagraphFormatting.d.ts +1 -1
  41. package/dist/types/lib/text/ParagraphFormatting.d.ts.map +1 -1
  42. package/dist/types/lib/text/PredicateEvaluator.d.ts +23 -0
  43. package/dist/types/lib/text/PredicateEvaluator.d.ts.map +1 -0
  44. package/dist/types/lib/text/index.d.ts +3 -1
  45. package/dist/types/lib/text/index.d.ts.map +1 -1
  46. package/dist/types/lib/text/types.d.ts +21 -0
  47. package/dist/types/lib/text/types.d.ts.map +1 -1
  48. package/dist/types/lib/types/index.d.ts +13 -0
  49. package/dist/types/lib/types/index.d.ts.map +1 -1
  50. package/package.json +2 -1
@@ -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
- * Render row loop indicators (colored stripe on left side of loop rows).
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 indicatorWidth = 4;
7851
- const labelPadding = 4;
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 color = loopColors[colorIndex % loopColors.length];
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(-indicatorWidth - 2, startY, indicatorWidth, loopHeight);
7878
- // Draw loop label on the first row
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
- ctx.fillStyle = color;
7882
- // Rotate text to be vertical along the stripe
7883
- const labelText = `⟳ ${loop.fieldPath}`;
7884
- const textMetrics = ctx.measureText(labelText);
7885
- // Position label to the left of the stripe
7886
- ctx.translate(-indicatorWidth - labelPadding - textMetrics.width - 4, startY + loopHeight / 2);
7887
- ctx.fillText(labelText, 0, 4);
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
- // Draw top and bottom brackets
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
- // Top bracket
7894
- ctx.moveTo(-indicatorWidth - 2, startY);
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
- * Toggle bullet list for the current paragraph (or selection).
10346
+ * Get paragraph starts affected by the current selection or cursor position.
9723
10347
  */
9724
- toggleBulletList() {
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 paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9728
- this.paragraphFormatting.toggleList(paragraphStart, 'bullet');
9729
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
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 (or selection).
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 paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9738
- this.paragraphFormatting.toggleList(paragraphStart, 'number');
9739
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
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 (increase list nesting level).
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 paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9748
- this.paragraphFormatting.indentParagraph(paragraphStart);
9749
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
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 (decrease list nesting level).
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 paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9758
- this.paragraphFormatting.outdentParagraph(paragraphStart);
9759
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
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; // No vertical line if neither start nor end
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 "Loop" label last so it's in front of all lines
15148
+ // Draw "If" label
13789
15149
  if (hasStart) {
13790
15150
  const startY = startInfo.y;
13791
- this.drawLoopLabel(ctx, labelX, startY - 10, 'Loop', isSelected);
15151
+ this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
13792
15152
  }
13793
- // Update section's visual state
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 "Loop" label in a rounded rectangle.
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
- drawLoopLabel(ctx, x, y, text, isSelected = false) {
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 + LOOP_LABEL_PADDING * 2;
13815
- const boxHeight = textHeight + LOOP_LABEL_PADDING * 2;
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, LOOP_LABEL_RADIUS);
15175
+ this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
13818
15176
  if (isSelected) {
13819
- // Selected: filled background with white text
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 = LOOP_INDICATOR_COLOR;
15184
+ ctx.strokeStyle = COND_INDICATOR_COLOR;
13829
15185
  ctx.lineWidth = 1.5;
13830
15186
  ctx.stroke();
13831
- ctx.fillStyle = LOOP_INDICATOR_COLOR;
15187
+ ctx.fillStyle = COND_INDICATOR_COLOR;
13832
15188
  }
13833
- // Draw text
13834
15189
  ctx.textBaseline = 'middle';
13835
- ctx.fillText(text, x + LOOP_LABEL_PADDING, y + boxHeight / 2);
15190
+ ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
13836
15191
  ctx.restore();
13837
15192
  }
13838
15193
  /**
13839
- * Draw a rounded rectangle path.
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
- getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
13897
- const labelX = pageBounds.x + 5;
13898
- const labelWidth = 32;
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; // Pixels for click detection
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 = this.sectionSpansPage(section, flowedPage);
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 Loop label
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
- // Check which section the object belongs to - only interact if in active section
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
- // Object is in a different section - ignore the interaction
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
- // Check which section the object belongs to
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
- // Object is in a different section - ignore the click
14932
- return;
16246
+ this.setActiveSection(objectSection);
14933
16247
  }
14934
- // Clicked on embedded object in the active section - clear text selection and select it
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
- // Only show interactive cursors for objects in the active section
15181
- const objectSection = this.getSectionForEmbeddedObject(object);
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 standardFont = getStandardFont(formatting.fontFamily || 'Arial', formatting.fontWeight, formatting.fontStyle);
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.pdfGenerator = new PDFGenerator();
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), arrow keys should deselect it
22355
- // and move the cursor in the text flow
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
- // Clear the selection and let the key be handled by the body content
22364
- this.canvasManager.clearSelection();
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: Expand table row loops in body, header, and footer
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 3: Substitute all fields in body
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 section after expansion
23797
- sectionManager.remove(section.id);
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
- Logger.log('[pc-editor] createRepeatingSection table row loop', fieldPath);
24311
- const row = focusedTable.focusedCell.row;
24312
- const loop = focusedTable.createRowLoop(row, row, fieldPath);
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
- fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
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
- fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
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 colorRow = this.createRow();
26487
- const colorGroup = document.createElement('div');
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
- colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26491
- colorRow.appendChild(colorGroup);
26492
- const highlightGroup = document.createElement('div');
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 highlightForm = this.createFormGroup('Highlight', this.highlightInput);
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
- highlightForm.appendChild(clearHighlightBtn);
26501
- highlightGroup.appendChild(highlightForm);
26502
- colorRow.appendChild(highlightGroup);
26503
- colorSection.appendChild(colorRow);
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
- if (event.type === 'repeating-section' && event.sectionId) {
27091
- const section = this.editor?.getRepeatingSection(event.sectionId);
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
- // Table row loop pane is typically shown manually when a table's row loop is selected
27237
- // The consumer is responsible for calling showLoop() with the table and loop
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, () => this.updateOffsetVisibility());
27411
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
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
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27419
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
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 section
27424
- const bgSection = this.createSection('Background');
29469
+ // Background - color on same row as label
29470
+ const bgSection = this.createSection();
27425
29471
  this.bgColorInput = this.createColorInput('#ffffff');
27426
- bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
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
- borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27434
- borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
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
- borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
29492
+ this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
29493
+ borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
27443
29494
  container.appendChild(borderSection);
27444
- // Padding section
27445
- const paddingSection = this.createSection('Padding');
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
- paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
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
- if (success) {
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, () => this.updateOffsetVisibility());
27653
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
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
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27661
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
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
- // Fit mode section
27666
- const fitSection = this.createSection('Display');
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
- fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
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
- fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27680
- container.appendChild(fitSection);
27681
- // Alt text section
27682
- const altSection = this.createSection('Accessibility');
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
- altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27685
- container.appendChild(altSection);
27686
- // Source section
27687
- const sourceSection = this.createSection('Source');
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
- sourceSection.appendChild(this.fileInput);
29744
+ container.appendChild(this.fileInput);
27694
29745
  const changeSourceBtn = this.createButton('Change Image...');
27695
29746
  this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27696
- sourceSection.appendChild(changeSourceBtn);
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-list';
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
- const rowInfo = document.createElement('div');
27925
- rowInfo.className = 'pc-pane-info';
27926
- rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
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/column buttons
27936
- const structureBtns = this.createButtonGroup();
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
- structureBtns.appendChild(addRowBtn);
27946
- structureBtns.appendChild(removeRowBtn);
27947
- structureBtns.appendChild(addColBtn);
27948
- structureBtns.appendChild(removeColBtn);
27949
- structureSection.appendChild(structureBtns);
27950
- container.appendChild(structureSection);
27951
- // Headers section
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
- headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27957
- headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27958
- headersSection.appendChild(headerRow);
27959
- const applyHeadersBtn = this.createButton('Apply Headers');
27960
- this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
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 defaultsRow = this.createRow();
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
- defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27969
- defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27970
- defaultsSection.appendChild(defaultsRow);
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
- // Background
30033
+ cellSection.appendChild(document.createElement('hr'));
30034
+ // Background — inline
27991
30035
  this.cellBgColorInput = this.createColorInput('#ffffff');
27992
- cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
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
- cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
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
- borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
28030
- borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
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
- cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
28039
- const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
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