@productcloudos/editor 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pc-editor.js CHANGED
@@ -2564,11 +2564,14 @@ class TextLayout {
2564
2564
  const emptyLine = this.createEmptyLine(startIndex, formatting, alignment);
2565
2565
  // Add list marker to empty line if it's a list item
2566
2566
  if (listFormatting) {
2567
+ // Get formatting for the empty line (inherited from previous position)
2568
+ const markerFormatting = formatting.getFormattingAt(startIndex > 0 ? startIndex - 1 : startIndex);
2567
2569
  emptyLine.listMarker = {
2568
2570
  text: markerText,
2569
2571
  width: markerWidth,
2570
2572
  indent: listIndent,
2571
- isFirstLineOfListItem: true
2573
+ isFirstLineOfListItem: true,
2574
+ formatting: markerFormatting
2572
2575
  };
2573
2576
  }
2574
2577
  lines.push(emptyLine);
@@ -3857,6 +3860,7 @@ class ImageObject extends BaseEmbeddedObject {
3857
3860
  const imgHeight = this._image.naturalHeight;
3858
3861
  let sx = 0, sy = 0, sw = imgWidth, sh = imgHeight;
3859
3862
  let dx = 0, dy = 0, dw = width, dh = height;
3863
+ let needsClipping = false;
3860
3864
  switch (this._fit) {
3861
3865
  case 'fill':
3862
3866
  // Stretch to fill
@@ -3880,18 +3884,31 @@ class ImageObject extends BaseEmbeddedObject {
3880
3884
  break;
3881
3885
  }
3882
3886
  case 'none':
3883
- // Original size, centered
3887
+ // Original size, centered - clip to box bounds if image is larger
3884
3888
  dw = imgWidth;
3885
3889
  dh = imgHeight;
3886
3890
  dx = (width - imgWidth) / 2;
3887
3891
  dy = (height - imgHeight) / 2;
3892
+ // Need clipping if image extends beyond bounds
3893
+ if (imgWidth > width || imgHeight > height) {
3894
+ needsClipping = true;
3895
+ }
3888
3896
  break;
3889
3897
  case 'tile':
3890
3898
  // Tile the image to fill bounds
3891
3899
  this.drawTiledImage(ctx, width, height);
3892
3900
  return; // Early return, tiling handles its own drawing
3893
3901
  }
3902
+ if (needsClipping) {
3903
+ ctx.save();
3904
+ ctx.beginPath();
3905
+ ctx.rect(0, 0, width, height);
3906
+ ctx.clip();
3907
+ }
3894
3908
  ctx.drawImage(this._image, sx, sy, sw, sh, dx, dy, dw, dh);
3909
+ if (needsClipping) {
3910
+ ctx.restore();
3911
+ }
3895
3912
  }
3896
3913
  drawTiledImage(ctx, width, height) {
3897
3914
  if (!this._image)
@@ -6407,8 +6424,9 @@ class TableObject extends BaseEmbeddedObject {
6407
6424
  }
6408
6425
  x += width;
6409
6426
  }
6410
- if (targetCol === -1)
6427
+ if (targetCol === -1) {
6411
6428
  return null;
6429
+ }
6412
6430
  // Calculate row positions
6413
6431
  let y = 0;
6414
6432
  let targetRow = -1;
@@ -6546,6 +6564,18 @@ class TableObject extends BaseEmbeddedObject {
6546
6564
  for (const row of this._rows) {
6547
6565
  row.removeCell(colIndex);
6548
6566
  }
6567
+ // Clear selection if it references the deleted column or is now out of bounds
6568
+ if (this._selectedRange) {
6569
+ const { start, end } = this._selectedRange;
6570
+ if (start.col >= this._columns.length || end.col >= this._columns.length ||
6571
+ (colIndex >= start.col && colIndex <= end.col)) {
6572
+ this._selectedRange = null;
6573
+ }
6574
+ }
6575
+ // Clear focused cell if it references the deleted column or is now out of bounds
6576
+ if (this._focusedCell && this._focusedCell.col >= this._columns.length) {
6577
+ this._focusedCell = null;
6578
+ }
6549
6579
  this._layoutDirty = true;
6550
6580
  this.updateCoveredCells();
6551
6581
  this.updateSizeFromLayout();
@@ -6604,6 +6634,18 @@ class TableObject extends BaseEmbeddedObject {
6604
6634
  const [removed] = this._rows.splice(rowIndex, 1);
6605
6635
  // Adjust row loop indices
6606
6636
  this.shiftRowLoopIndices(rowIndex, -1);
6637
+ // Clear selection if it references the deleted row or is now out of bounds
6638
+ if (this._selectedRange) {
6639
+ const { start, end } = this._selectedRange;
6640
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6641
+ (rowIndex >= start.row && rowIndex <= end.row)) {
6642
+ this._selectedRange = null;
6643
+ }
6644
+ }
6645
+ // Clear focused cell if it references the deleted row or is now out of bounds
6646
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6647
+ this._focusedCell = null;
6648
+ }
6607
6649
  this._layoutDirty = true;
6608
6650
  this.updateCoveredCells();
6609
6651
  this.updateSizeFromLayout();
@@ -6775,6 +6817,18 @@ class TableObject extends BaseEmbeddedObject {
6775
6817
  removeRowsInRange(startIndex, endIndex) {
6776
6818
  const count = endIndex - startIndex + 1;
6777
6819
  const removed = this._rows.splice(startIndex, count);
6820
+ // Clear selection if it overlaps with removed rows or is now out of bounds
6821
+ if (this._selectedRange) {
6822
+ const { start, end } = this._selectedRange;
6823
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6824
+ (start.row <= endIndex && end.row >= startIndex)) {
6825
+ this._selectedRange = null;
6826
+ }
6827
+ }
6828
+ // Clear focused cell if it's now out of bounds
6829
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6830
+ this._focusedCell = null;
6831
+ }
6778
6832
  this._layoutDirty = true;
6779
6833
  this.updateCoveredCells();
6780
6834
  return removed;
@@ -7198,8 +7252,74 @@ class TableObject extends BaseEmbeddedObject {
7198
7252
  }
7199
7253
  y += row.calculatedHeight;
7200
7254
  }
7255
+ // Render cell range selection highlight for this slice
7256
+ if (this._selectedRange) {
7257
+ this.renderRangeSelectionForSlice(ctx, slice, pageLayout);
7258
+ }
7201
7259
  // Note: Selection border is drawn by FlowingTextRenderer with correct slice height
7202
7260
  }
7261
+ /**
7262
+ * Render range selection highlight for a specific slice.
7263
+ * Only renders the portion of the selection that's visible in this slice.
7264
+ */
7265
+ renderRangeSelectionForSlice(ctx, slice, pageLayout) {
7266
+ if (!this._selectedRange)
7267
+ return;
7268
+ const { start, end } = this._selectedRange;
7269
+ const columnPositions = this.getColumnPositions();
7270
+ const columnWidths = this.getColumnWidths();
7271
+ // Calculate X bounds (same for all slices)
7272
+ const x1 = columnPositions[start.col];
7273
+ const x2 = columnPositions[end.col] + columnWidths[end.col];
7274
+ // Build a map of row index -> Y position in this slice's coordinate system
7275
+ const rowYInSlice = new Map();
7276
+ let y = 0;
7277
+ // On continuation pages, header rows are at the top
7278
+ if (slice.isContinuation && pageLayout.headerRowIndices.length > 0) {
7279
+ for (const headerRowIdx of pageLayout.headerRowIndices) {
7280
+ const row = this._rows[headerRowIdx];
7281
+ if (row) {
7282
+ rowYInSlice.set(headerRowIdx, y);
7283
+ y += row.calculatedHeight;
7284
+ }
7285
+ }
7286
+ }
7287
+ // Data rows for this slice
7288
+ for (let rowIdx = slice.startRow; rowIdx < slice.endRow; rowIdx++) {
7289
+ const row = this._rows[rowIdx];
7290
+ if (!row)
7291
+ continue;
7292
+ if (slice.isContinuation && row.isHeader)
7293
+ continue; // Skip headers, already added
7294
+ rowYInSlice.set(rowIdx, y);
7295
+ y += row.calculatedHeight;
7296
+ }
7297
+ // Check if any selected rows are visible in this slice
7298
+ let y1 = null;
7299
+ let y2 = null;
7300
+ for (let rowIdx = start.row; rowIdx <= end.row; rowIdx++) {
7301
+ const rowY = rowYInSlice.get(rowIdx);
7302
+ if (rowY !== undefined) {
7303
+ const row = this._rows[rowIdx];
7304
+ if (row) {
7305
+ if (y1 === null)
7306
+ y1 = rowY;
7307
+ y2 = rowY + row.calculatedHeight;
7308
+ }
7309
+ }
7310
+ }
7311
+ // If no selected rows are visible in this slice, don't render
7312
+ if (y1 === null || y2 === null)
7313
+ return;
7314
+ // Draw selection highlight
7315
+ ctx.fillStyle = 'rgba(0, 120, 215, 0.2)';
7316
+ ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
7317
+ // Draw selection border
7318
+ ctx.strokeStyle = 'rgba(0, 120, 215, 0.8)';
7319
+ ctx.lineWidth = 2;
7320
+ ctx.setLineDash([]);
7321
+ ctx.strokeRect(x1 + 1, y1 + 1, x2 - x1 - 2, y2 - y1 - 2);
7322
+ }
7203
7323
  /**
7204
7324
  * Get rows that should be rendered for a specific page slice.
7205
7325
  * Includes header rows on continuation pages.
@@ -7366,6 +7486,8 @@ class TableObject extends BaseEmbeddedObject {
7366
7486
  }
7367
7487
  blur() {
7368
7488
  this.editing = false;
7489
+ // Clear cell range selection when table loses focus
7490
+ this.clearSelection();
7369
7491
  this.emit('blur', {});
7370
7492
  }
7371
7493
  hasFocus() {
@@ -11432,7 +11554,9 @@ class FlowingTextRenderer extends EventEmitter {
11432
11554
  const maxWidth = this.getAvailableWidthForRegion(region, pageIndex);
11433
11555
  const flowingContent = region.flowingContent;
11434
11556
  // Get cursor position for field selection highlighting
11435
- const cursorTextIndex = flowingContent.getCursorPosition();
11557
+ // Only use cursor position if the content has focus (otherwise fields stay "selected")
11558
+ const hasFocus = flowingContent.hasFocus();
11559
+ const cursorTextIndex = hasFocus ? flowingContent.getCursorPosition() : undefined;
11436
11560
  // Setup clipping if requested (useful for text boxes)
11437
11561
  if (clipToBounds) {
11438
11562
  ctx.save();
@@ -11469,7 +11593,7 @@ class FlowingTextRenderer extends EventEmitter {
11469
11593
  }
11470
11594
  // Render cursor if this region is active and cursor should be shown
11471
11595
  if (renderCursor && flowingContent.hasFocus() && flowingContent.isCursorVisible()) {
11472
- this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, cursorTextIndex);
11596
+ this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, flowingContent.getCursorPosition());
11473
11597
  }
11474
11598
  if (clipToBounds) {
11475
11599
  ctx.restore();
@@ -11577,8 +11701,10 @@ class FlowingTextRenderer extends EventEmitter {
11577
11701
  this.pageTextOffsets.set(pageIndex, 0);
11578
11702
  }
11579
11703
  // Get cursor position from the specified FlowingTextContent, or fall back to body
11704
+ // Only use cursor position for field selection if the content has focus
11580
11705
  const contentForCursor = flowingContent || this.document.bodyFlowingContent;
11581
- const cursorTextIndex = contentForCursor ? contentForCursor.getCursorPosition() : 0;
11706
+ const hasFocus = contentForCursor?.hasFocus() ?? false;
11707
+ const cursorTextIndex = hasFocus && contentForCursor ? contentForCursor.getCursorPosition() : undefined;
11582
11708
  // Get total page count for page count fields
11583
11709
  const firstPage = this.document.pages[0];
11584
11710
  const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
@@ -11660,7 +11786,7 @@ class FlowingTextRenderer extends EventEmitter {
11660
11786
  ctx.textBaseline = 'alphabetic';
11661
11787
  // Render list marker if this is the first line of a list item
11662
11788
  if (line.listMarker?.isFirstLineOfListItem && line.listMarker.text) {
11663
- this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting);
11789
+ this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting || line.listMarker.formatting);
11664
11790
  }
11665
11791
  // Create maps for quick lookup by text index
11666
11792
  const substitutionFieldMap = new Map();
@@ -12894,11 +13020,14 @@ class FlowingTextRenderer extends EventEmitter {
12894
13020
  for (const line of flowedPage.lines) {
12895
13021
  if (this.lineContainsSelection(line, this.selectedText)) {
12896
13022
  const selectionBounds = this.getSelectionBoundsInLine(line, this.selectedText);
12897
- // Add alignment offset to position the selection correctly
12898
- const alignmentOffset = this.getAlignmentOffset(line, bounds.width);
12899
- // Account for list indentation
13023
+ // Account for list indentation - must match renderFlowedLine calculation
12900
13024
  const listIndent = line.listMarker?.indent ?? 0;
12901
- ctx.fillRect(bounds.x + alignmentOffset + listIndent + selectionBounds.x, y, selectionBounds.width, line.height);
13025
+ // Calculate alignment offset using effective width (excluding list indent)
13026
+ // This matches how renderFlowedLine calculates alignment
13027
+ const effectiveMaxWidth = bounds.width - listIndent;
13028
+ const alignmentOffset = this.getAlignmentOffset(line, effectiveMaxWidth);
13029
+ const baseX = bounds.x + listIndent;
13030
+ ctx.fillRect(baseX + alignmentOffset + selectionBounds.x, y, selectionBounds.width, line.height);
12902
13031
  }
12903
13032
  y += line.height;
12904
13033
  }
@@ -13371,15 +13500,34 @@ class FlowingTextRenderer extends EventEmitter {
13371
13500
  }
13372
13501
  /**
13373
13502
  * Get a complete snapshot of all flowed content for PDF export.
13374
- * Returns body pages, header, and footer content.
13503
+ * Returns body pages, header, footer content, and hyperlinks.
13375
13504
  */
13376
13505
  getFlowedPagesSnapshot() {
13377
13506
  const firstPage = this.document.pages[0];
13378
13507
  const bodyPages = firstPage ? this.flowedPages.get(firstPage.id) || [] : [];
13508
+ // Extract hyperlinks from each content area
13509
+ const bodyHyperlinks = this.document.bodyFlowingContent?.getAllHyperlinks().map(h => ({
13510
+ url: h.url,
13511
+ startIndex: h.startIndex,
13512
+ endIndex: h.endIndex
13513
+ }));
13514
+ const headerHyperlinks = this.document.headerFlowingContent?.getAllHyperlinks().map(h => ({
13515
+ url: h.url,
13516
+ startIndex: h.startIndex,
13517
+ endIndex: h.endIndex
13518
+ }));
13519
+ const footerHyperlinks = this.document.footerFlowingContent?.getAllHyperlinks().map(h => ({
13520
+ url: h.url,
13521
+ startIndex: h.startIndex,
13522
+ endIndex: h.endIndex
13523
+ }));
13379
13524
  return {
13380
13525
  body: bodyPages,
13381
13526
  header: this.headerFlowedPage,
13382
- footer: this.footerFlowedPage
13527
+ footer: this.footerFlowedPage,
13528
+ bodyHyperlinks: bodyHyperlinks?.length ? bodyHyperlinks : undefined,
13529
+ headerHyperlinks: headerHyperlinks?.length ? headerHyperlinks : undefined,
13530
+ footerHyperlinks: footerHyperlinks?.length ? footerHyperlinks : undefined
13383
13531
  };
13384
13532
  }
13385
13533
  // ============================================
@@ -13809,6 +13957,25 @@ class CanvasManager extends EventEmitter {
13809
13957
  this.canvases.clear();
13810
13958
  this.contexts.clear();
13811
13959
  }
13960
+ /**
13961
+ * Update canvas sizes to match current page dimensions.
13962
+ * Call this when page size or orientation changes.
13963
+ */
13964
+ updateCanvasSizes() {
13965
+ this.document.pages.forEach(page => {
13966
+ const canvas = this.canvases.get(page.id);
13967
+ if (!canvas)
13968
+ return;
13969
+ const dimensions = page.getPageDimensions();
13970
+ // Only update if dimensions have changed
13971
+ if (canvas.width !== dimensions.width || canvas.height !== dimensions.height) {
13972
+ canvas.width = dimensions.width;
13973
+ canvas.height = dimensions.height;
13974
+ }
13975
+ });
13976
+ // Update zoom scale to account for new dimensions
13977
+ this.updateCanvasScale();
13978
+ }
13812
13979
  render() {
13813
13980
  this.document.pages.forEach(page => {
13814
13981
  const ctx = this.contexts.get(page.id);
@@ -14148,6 +14315,12 @@ class CanvasManager extends EventEmitter {
14148
14315
  const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
14149
14316
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14150
14317
  const object = embeddedObjectHit.data.object;
14318
+ // Check which section the object belongs to - only interact if in active section
14319
+ const objectSection = this.getSectionForEmbeddedObject(object);
14320
+ if (objectSection && objectSection !== this._activeSection) {
14321
+ // Object is in a different section - ignore the interaction
14322
+ return;
14323
+ }
14151
14324
  // For relative-positioned objects, prepare for potential drag
14152
14325
  // Don't start drag immediately - wait for threshold to allow double-click
14153
14326
  if (object.position === 'relative') {
@@ -14316,19 +14489,40 @@ class CanvasManager extends EventEmitter {
14316
14489
  // Handle table cell selection drag
14317
14490
  if (this.isSelectingTableCells && this.tableCellSelectionStart && this.tableCellSelectionTable) {
14318
14491
  const table = this.tableCellSelectionTable;
14319
- if (table.renderedPosition) {
14320
- const localPoint = {
14321
- x: point.x - table.renderedPosition.x,
14322
- y: point.y - table.renderedPosition.y
14323
- };
14324
- const cellAddr = table.getCellAtPoint(localPoint);
14325
- if (cellAddr) {
14326
- // Update selection range
14327
- table.selectRange({
14328
- start: this.tableCellSelectionStart,
14329
- end: cellAddr
14330
- });
14331
- this.render();
14492
+ const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14493
+ // Get the slice for the current page (for multi-page tables)
14494
+ const slice = table.getRenderedSlice(currentPageIndex);
14495
+ const tablePosition = slice?.position || table.renderedPosition;
14496
+ const sliceHeight = slice?.height || table.height;
14497
+ if (tablePosition) {
14498
+ // Check if point is within the table slice on this page
14499
+ const isInsideTable = point.x >= tablePosition.x &&
14500
+ point.x <= tablePosition.x + table.width &&
14501
+ point.y >= tablePosition.y &&
14502
+ point.y <= tablePosition.y + sliceHeight;
14503
+ if (isInsideTable) {
14504
+ const localPoint = {
14505
+ x: point.x - tablePosition.x,
14506
+ y: point.y - tablePosition.y
14507
+ };
14508
+ // If this is a continuation slice, adjust y for the slice offset
14509
+ if (slice && (slice.slicePosition === 'middle' || slice.slicePosition === 'last')) {
14510
+ const headerHeight = slice.headerHeight;
14511
+ if (localPoint.y >= headerHeight) {
14512
+ // Click is in the data rows area - transform coordinates
14513
+ localPoint.y = slice.yOffset + (localPoint.y - headerHeight);
14514
+ }
14515
+ // If y < headerHeight, click is in repeated header - no adjustment needed
14516
+ }
14517
+ const cellAddr = table.getCellAtPoint(localPoint);
14518
+ if (cellAddr) {
14519
+ // Update selection range
14520
+ table.selectRange({
14521
+ start: this.tableCellSelectionStart,
14522
+ end: cellAddr
14523
+ });
14524
+ this.render();
14525
+ }
14332
14526
  }
14333
14527
  }
14334
14528
  e.preventDefault();
@@ -14437,6 +14631,10 @@ class CanvasManager extends EventEmitter {
14437
14631
  });
14438
14632
  }
14439
14633
  }
14634
+ // Re-render to update rendered positions, then update resize handle hit targets
14635
+ // This fixes the bug where resize handles don't work after a resize operation
14636
+ this.render();
14637
+ this.updateResizeHandleHitTargets();
14440
14638
  }
14441
14639
  this.isResizing = false;
14442
14640
  this.dragStart = null;
@@ -14605,13 +14803,21 @@ class CanvasManager extends EventEmitter {
14605
14803
  const hitTestManager = this.flowingTextRenderer.hitTestManager;
14606
14804
  const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
14607
14805
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14608
- // Clicked on embedded object - clear text selection and select it
14806
+ const clickedObject = embeddedObjectHit.data.object;
14807
+ // Check which section the object belongs to
14808
+ const objectSection = this.getSectionForEmbeddedObject(clickedObject);
14809
+ // Only allow selection if object is in the active section
14810
+ if (objectSection && objectSection !== this._activeSection) {
14811
+ // Object is in a different section - ignore the click
14812
+ return;
14813
+ }
14814
+ // Clicked on embedded object in the active section - clear text selection and select it
14609
14815
  const activeFlowingContent = this.getFlowingContentForActiveSection();
14610
14816
  if (activeFlowingContent) {
14611
14817
  activeFlowingContent.clearSelection();
14612
14818
  }
14613
14819
  this.clearSelection();
14614
- this.selectInlineElement(embeddedObjectHit.data.object);
14820
+ this.selectInlineElement(clickedObject);
14615
14821
  return;
14616
14822
  }
14617
14823
  // First check if we clicked on a repeating section indicator
@@ -14851,11 +15057,50 @@ class CanvasManager extends EventEmitter {
14851
15057
  const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
14852
15058
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14853
15059
  const object = embeddedObjectHit.data.object;
14854
- if (object.position === 'relative') {
14855
- canvas.style.cursor = 'move';
14856
- return;
15060
+ // Only show interactive cursors for objects in the active section
15061
+ const objectSection = this.getSectionForEmbeddedObject(object);
15062
+ if (objectSection && objectSection !== this._activeSection) ;
15063
+ else {
15064
+ if (object.position === 'relative') {
15065
+ canvas.style.cursor = 'move';
15066
+ return;
15067
+ }
15068
+ // Show text cursor for text boxes
15069
+ if (object instanceof TextBoxObject) {
15070
+ canvas.style.cursor = 'text';
15071
+ return;
15072
+ }
14857
15073
  }
14858
15074
  }
15075
+ // Check for table cells (show text cursor)
15076
+ const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15077
+ if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15078
+ canvas.style.cursor = 'text';
15079
+ return;
15080
+ }
15081
+ // Check for text regions (body, header, footer - show text cursor)
15082
+ const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15083
+ if (textRegionHit && textRegionHit.data.type === 'text-region') {
15084
+ canvas.style.cursor = 'text';
15085
+ return;
15086
+ }
15087
+ // Also check if point is within any editable region (body, header, footer)
15088
+ // This catches cases where text region hit targets may not cover empty space
15089
+ const bodyRegion = this.regionManager.getBodyRegion();
15090
+ if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15091
+ canvas.style.cursor = 'text';
15092
+ return;
15093
+ }
15094
+ const headerRegion = this.regionManager.getHeaderRegion();
15095
+ if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15096
+ canvas.style.cursor = 'text';
15097
+ return;
15098
+ }
15099
+ const footerRegion = this.regionManager.getFooterRegion();
15100
+ if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15101
+ canvas.style.cursor = 'text';
15102
+ return;
15103
+ }
14859
15104
  canvas.style.cursor = 'default';
14860
15105
  }
14861
15106
  /**
@@ -15032,6 +15277,9 @@ class CanvasManager extends EventEmitter {
15032
15277
  return this.selectedElements.size > 0;
15033
15278
  }
15034
15279
  selectBaseEmbeddedObject(embeddedObject, isPartOfRangeSelection = false) {
15280
+ // Clear focus from any currently focused control (e.g., header/footer text)
15281
+ // This ensures cursor stops blinking in the previous section
15282
+ this.setFocus(null);
15035
15283
  // Mark the embedded object as selected
15036
15284
  const obj = embeddedObject.object || embeddedObject;
15037
15285
  obj.selected = true;
@@ -15498,6 +15746,27 @@ class CanvasManager extends EventEmitter {
15498
15746
  this.emit('section-focus-changed', { section, previousSection });
15499
15747
  }
15500
15748
  }
15749
+ /**
15750
+ * Determine which section (body/header/footer) an embedded object belongs to.
15751
+ */
15752
+ getSectionForEmbeddedObject(object) {
15753
+ const sectionMappings = [
15754
+ { content: this.document.bodyFlowingContent, section: 'body' },
15755
+ { content: this.document.headerFlowingContent, section: 'header' },
15756
+ { content: this.document.footerFlowingContent, section: 'footer' }
15757
+ ];
15758
+ for (const { content, section } of sectionMappings) {
15759
+ if (!content)
15760
+ continue;
15761
+ const embeddedObjects = content.getEmbeddedObjects();
15762
+ for (const [, embeddedObj] of embeddedObjects.entries()) {
15763
+ if (embeddedObj.id === object.id) {
15764
+ return section;
15765
+ }
15766
+ }
15767
+ }
15768
+ return null;
15769
+ }
15501
15770
  /**
15502
15771
  * Detect which section a point is in based on Y coordinate.
15503
15772
  * Uses full page width areas (not just content bounds).
@@ -16111,11 +16380,11 @@ class PDFGenerator {
16111
16380
  width: headerRegion.size.width,
16112
16381
  height: headerRegion.size.height
16113
16382
  };
16114
- await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length);
16383
+ await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.headerHyperlinks);
16115
16384
  }
16116
16385
  // Render body content
16117
16386
  if (flowedContent?.body && flowedContent.body[pageIndex]) {
16118
- await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length);
16387
+ await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.bodyHyperlinks);
16119
16388
  }
16120
16389
  // Render footer if present
16121
16390
  if (flowedContent?.footer && document.footerFlowingContent) {
@@ -16126,7 +16395,7 @@ class PDFGenerator {
16126
16395
  width: footerRegion.size.width,
16127
16396
  height: footerRegion.size.height
16128
16397
  };
16129
- await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length);
16398
+ await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.footerHyperlinks);
16130
16399
  }
16131
16400
  }
16132
16401
  catch (pageError) {
@@ -16211,10 +16480,12 @@ class PDFGenerator {
16211
16480
  /**
16212
16481
  * Render a flowed page to PDF.
16213
16482
  */
16214
- async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages) {
16483
+ async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages, hyperlinks) {
16215
16484
  let y = bounds.y;
16216
16485
  // Track relative objects to render after all lines (so they appear on top)
16217
16486
  const relativeObjects = [];
16487
+ // Track rendered text positions for hyperlink annotations
16488
+ const renderedRuns = [];
16218
16489
  for (const line of flowedPage.lines) {
16219
16490
  // Collect relative objects from this line
16220
16491
  if (line.embeddedObjects) {
@@ -16245,10 +16516,10 @@ class PDFGenerator {
16245
16516
  const font = this.getFont(formatting);
16246
16517
  const fontSize = formatting.fontSize || 14;
16247
16518
  const color = parseColor(formatting.color || '#000000');
16519
+ const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16248
16520
  // Draw background if present
16249
16521
  if (formatting.backgroundColor) {
16250
16522
  const bgColor = parseColor(formatting.backgroundColor);
16251
- const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16252
16523
  drawFilledRect(pdfPage, runX, y, textWidth, line.height, bgColor, pageHeight);
16253
16524
  }
16254
16525
  // Draw text - position at baseline
@@ -16260,8 +16531,19 @@ class PDFGenerator {
16260
16531
  size: fontSize,
16261
16532
  color
16262
16533
  });
16534
+ // Track this run's position for hyperlink annotations
16535
+ if (hyperlinks && hyperlinks.length > 0) {
16536
+ renderedRuns.push({
16537
+ x: runX,
16538
+ y: y,
16539
+ width: textWidth,
16540
+ height: line.height,
16541
+ startIndex: run.startIndex,
16542
+ endIndex: run.endIndex
16543
+ });
16544
+ }
16263
16545
  // Advance X position
16264
- runX += font.widthOfTextAtSize(safeText, fontSize);
16546
+ runX += textWidth;
16265
16547
  // Add extra word spacing for justified text
16266
16548
  if (line.extraWordSpacing && safeText.includes(' ')) {
16267
16549
  const spaceCount = (safeText.match(/ /g) || []).length;
@@ -16299,6 +16581,62 @@ class PDFGenerator {
16299
16581
  }
16300
16582
  // Render relative objects last (so they appear on top of text)
16301
16583
  await this.renderRelativeObjects(pdfPage, relativeObjects, pageHeight, pageIndex, totalPages);
16584
+ // Create hyperlink annotations
16585
+ if (hyperlinks && hyperlinks.length > 0 && renderedRuns.length > 0) {
16586
+ this.createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight);
16587
+ }
16588
+ }
16589
+ /**
16590
+ * Create PDF link annotations for hyperlinks.
16591
+ * Matches hyperlink ranges with rendered text positions and creates clickable links.
16592
+ */
16593
+ createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight) {
16594
+ for (const hyperlink of hyperlinks) {
16595
+ // Find all runs that overlap with this hyperlink
16596
+ const overlappingRuns = renderedRuns.filter(run => run.startIndex < hyperlink.endIndex && run.endIndex > hyperlink.startIndex);
16597
+ if (overlappingRuns.length === 0)
16598
+ continue;
16599
+ // Calculate bounding box for the hyperlink
16600
+ // For multi-line hyperlinks, we create separate annotations for each line segment
16601
+ // Group runs by their Y position (same line)
16602
+ const runsByLine = new Map();
16603
+ for (const run of overlappingRuns) {
16604
+ const key = run.y;
16605
+ if (!runsByLine.has(key)) {
16606
+ runsByLine.set(key, []);
16607
+ }
16608
+ runsByLine.get(key).push(run);
16609
+ }
16610
+ // Create an annotation for each line segment
16611
+ for (const [lineY, lineRuns] of runsByLine) {
16612
+ // Calculate bounds for this line's portion of the hyperlink
16613
+ const minX = Math.min(...lineRuns.map(r => r.x));
16614
+ const maxX = Math.max(...lineRuns.map(r => r.x + r.width));
16615
+ const height = lineRuns[0].height;
16616
+ // Transform to PDF coordinates (Y is inverted)
16617
+ const pdfY = pageHeight - lineY - height;
16618
+ // Create link annotation using pdf-lib
16619
+ const linkAnnotation = pdfPage.doc.context.obj({
16620
+ Type: 'Annot',
16621
+ Subtype: 'Link',
16622
+ Rect: [minX, pdfY, maxX, pdfY + height],
16623
+ Border: [0, 0, 0], // No visible border
16624
+ A: {
16625
+ Type: 'Action',
16626
+ S: 'URI',
16627
+ URI: hyperlink.url
16628
+ }
16629
+ });
16630
+ // Add annotation to page
16631
+ const annotations = pdfPage.node.get(pdfPage.doc.context.obj('Annots'));
16632
+ if (annotations) {
16633
+ annotations.push(linkAnnotation);
16634
+ }
16635
+ else {
16636
+ pdfPage.node.set(pdfPage.doc.context.obj('Annots'), pdfPage.doc.context.obj([linkAnnotation]));
16637
+ }
16638
+ }
16639
+ }
16302
16640
  }
16303
16641
  /**
16304
16642
  * Calculate alignment offset for a line.
@@ -20924,8 +21262,12 @@ class PCEditor extends EventEmitter {
20924
21262
  }
20925
21263
  });
20926
21264
  this.document.on('settings-changed', () => {
20927
- // When settings change (like margins), we need to check if pages need to be added/removed
21265
+ // When settings change (like page size, orientation, margins), we need to:
21266
+ // 1. Update canvas sizes to match new page dimensions
21267
+ // 2. Re-render the content
21268
+ // 3. Check if pages need to be added/removed
20928
21269
  if (this.canvasManager) {
21270
+ this.canvasManager.updateCanvasSizes();
20929
21271
  this.canvasManager.render();
20930
21272
  // Defer the page check to allow reflow to complete
20931
21273
  setTimeout(() => {