@productcloudos/editor 1.0.5 → 1.0.7

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