@productcloudos/editor 1.0.1 → 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.
@@ -2543,11 +2543,14 @@ class TextLayout {
2543
2543
  const emptyLine = this.createEmptyLine(startIndex, formatting, alignment);
2544
2544
  // Add list marker to empty line if it's a list item
2545
2545
  if (listFormatting) {
2546
+ // Get formatting for the empty line (inherited from previous position)
2547
+ const markerFormatting = formatting.getFormattingAt(startIndex > 0 ? startIndex - 1 : startIndex);
2546
2548
  emptyLine.listMarker = {
2547
2549
  text: markerText,
2548
2550
  width: markerWidth,
2549
2551
  indent: listIndent,
2550
- isFirstLineOfListItem: true
2552
+ isFirstLineOfListItem: true,
2553
+ formatting: markerFormatting
2551
2554
  };
2552
2555
  }
2553
2556
  lines.push(emptyLine);
@@ -3836,6 +3839,7 @@ class ImageObject extends BaseEmbeddedObject {
3836
3839
  const imgHeight = this._image.naturalHeight;
3837
3840
  let sx = 0, sy = 0, sw = imgWidth, sh = imgHeight;
3838
3841
  let dx = 0, dy = 0, dw = width, dh = height;
3842
+ let needsClipping = false;
3839
3843
  switch (this._fit) {
3840
3844
  case 'fill':
3841
3845
  // Stretch to fill
@@ -3859,18 +3863,31 @@ class ImageObject extends BaseEmbeddedObject {
3859
3863
  break;
3860
3864
  }
3861
3865
  case 'none':
3862
- // Original size, centered
3866
+ // Original size, centered - clip to box bounds if image is larger
3863
3867
  dw = imgWidth;
3864
3868
  dh = imgHeight;
3865
3869
  dx = (width - imgWidth) / 2;
3866
3870
  dy = (height - imgHeight) / 2;
3871
+ // Need clipping if image extends beyond bounds
3872
+ if (imgWidth > width || imgHeight > height) {
3873
+ needsClipping = true;
3874
+ }
3867
3875
  break;
3868
3876
  case 'tile':
3869
3877
  // Tile the image to fill bounds
3870
3878
  this.drawTiledImage(ctx, width, height);
3871
3879
  return; // Early return, tiling handles its own drawing
3872
3880
  }
3881
+ if (needsClipping) {
3882
+ ctx.save();
3883
+ ctx.beginPath();
3884
+ ctx.rect(0, 0, width, height);
3885
+ ctx.clip();
3886
+ }
3873
3887
  ctx.drawImage(this._image, sx, sy, sw, sh, dx, dy, dw, dh);
3888
+ if (needsClipping) {
3889
+ ctx.restore();
3890
+ }
3874
3891
  }
3875
3892
  drawTiledImage(ctx, width, height) {
3876
3893
  if (!this._image)
@@ -6386,8 +6403,9 @@ class TableObject extends BaseEmbeddedObject {
6386
6403
  }
6387
6404
  x += width;
6388
6405
  }
6389
- if (targetCol === -1)
6406
+ if (targetCol === -1) {
6390
6407
  return null;
6408
+ }
6391
6409
  // Calculate row positions
6392
6410
  let y = 0;
6393
6411
  let targetRow = -1;
@@ -6525,6 +6543,18 @@ class TableObject extends BaseEmbeddedObject {
6525
6543
  for (const row of this._rows) {
6526
6544
  row.removeCell(colIndex);
6527
6545
  }
6546
+ // Clear selection if it references the deleted column or is now out of bounds
6547
+ if (this._selectedRange) {
6548
+ const { start, end } = this._selectedRange;
6549
+ if (start.col >= this._columns.length || end.col >= this._columns.length ||
6550
+ (colIndex >= start.col && colIndex <= end.col)) {
6551
+ this._selectedRange = null;
6552
+ }
6553
+ }
6554
+ // Clear focused cell if it references the deleted column or is now out of bounds
6555
+ if (this._focusedCell && this._focusedCell.col >= this._columns.length) {
6556
+ this._focusedCell = null;
6557
+ }
6528
6558
  this._layoutDirty = true;
6529
6559
  this.updateCoveredCells();
6530
6560
  this.updateSizeFromLayout();
@@ -6583,6 +6613,18 @@ class TableObject extends BaseEmbeddedObject {
6583
6613
  const [removed] = this._rows.splice(rowIndex, 1);
6584
6614
  // Adjust row loop indices
6585
6615
  this.shiftRowLoopIndices(rowIndex, -1);
6616
+ // Clear selection if it references the deleted row or is now out of bounds
6617
+ if (this._selectedRange) {
6618
+ const { start, end } = this._selectedRange;
6619
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6620
+ (rowIndex >= start.row && rowIndex <= end.row)) {
6621
+ this._selectedRange = null;
6622
+ }
6623
+ }
6624
+ // Clear focused cell if it references the deleted row or is now out of bounds
6625
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6626
+ this._focusedCell = null;
6627
+ }
6586
6628
  this._layoutDirty = true;
6587
6629
  this.updateCoveredCells();
6588
6630
  this.updateSizeFromLayout();
@@ -6754,6 +6796,18 @@ class TableObject extends BaseEmbeddedObject {
6754
6796
  removeRowsInRange(startIndex, endIndex) {
6755
6797
  const count = endIndex - startIndex + 1;
6756
6798
  const removed = this._rows.splice(startIndex, count);
6799
+ // Clear selection if it overlaps with removed rows or is now out of bounds
6800
+ if (this._selectedRange) {
6801
+ const { start, end } = this._selectedRange;
6802
+ if (start.row >= this._rows.length || end.row >= this._rows.length ||
6803
+ (start.row <= endIndex && end.row >= startIndex)) {
6804
+ this._selectedRange = null;
6805
+ }
6806
+ }
6807
+ // Clear focused cell if it's now out of bounds
6808
+ if (this._focusedCell && this._focusedCell.row >= this._rows.length) {
6809
+ this._focusedCell = null;
6810
+ }
6757
6811
  this._layoutDirty = true;
6758
6812
  this.updateCoveredCells();
6759
6813
  return removed;
@@ -7177,8 +7231,74 @@ class TableObject extends BaseEmbeddedObject {
7177
7231
  }
7178
7232
  y += row.calculatedHeight;
7179
7233
  }
7234
+ // Render cell range selection highlight for this slice
7235
+ if (this._selectedRange) {
7236
+ this.renderRangeSelectionForSlice(ctx, slice, pageLayout);
7237
+ }
7180
7238
  // Note: Selection border is drawn by FlowingTextRenderer with correct slice height
7181
7239
  }
7240
+ /**
7241
+ * Render range selection highlight for a specific slice.
7242
+ * Only renders the portion of the selection that's visible in this slice.
7243
+ */
7244
+ renderRangeSelectionForSlice(ctx, slice, pageLayout) {
7245
+ if (!this._selectedRange)
7246
+ return;
7247
+ const { start, end } = this._selectedRange;
7248
+ const columnPositions = this.getColumnPositions();
7249
+ const columnWidths = this.getColumnWidths();
7250
+ // Calculate X bounds (same for all slices)
7251
+ const x1 = columnPositions[start.col];
7252
+ const x2 = columnPositions[end.col] + columnWidths[end.col];
7253
+ // Build a map of row index -> Y position in this slice's coordinate system
7254
+ const rowYInSlice = new Map();
7255
+ let y = 0;
7256
+ // On continuation pages, header rows are at the top
7257
+ if (slice.isContinuation && pageLayout.headerRowIndices.length > 0) {
7258
+ for (const headerRowIdx of pageLayout.headerRowIndices) {
7259
+ const row = this._rows[headerRowIdx];
7260
+ if (row) {
7261
+ rowYInSlice.set(headerRowIdx, y);
7262
+ y += row.calculatedHeight;
7263
+ }
7264
+ }
7265
+ }
7266
+ // Data rows for this slice
7267
+ for (let rowIdx = slice.startRow; rowIdx < slice.endRow; rowIdx++) {
7268
+ const row = this._rows[rowIdx];
7269
+ if (!row)
7270
+ continue;
7271
+ if (slice.isContinuation && row.isHeader)
7272
+ continue; // Skip headers, already added
7273
+ rowYInSlice.set(rowIdx, y);
7274
+ y += row.calculatedHeight;
7275
+ }
7276
+ // Check if any selected rows are visible in this slice
7277
+ let y1 = null;
7278
+ let y2 = null;
7279
+ for (let rowIdx = start.row; rowIdx <= end.row; rowIdx++) {
7280
+ const rowY = rowYInSlice.get(rowIdx);
7281
+ if (rowY !== undefined) {
7282
+ const row = this._rows[rowIdx];
7283
+ if (row) {
7284
+ if (y1 === null)
7285
+ y1 = rowY;
7286
+ y2 = rowY + row.calculatedHeight;
7287
+ }
7288
+ }
7289
+ }
7290
+ // If no selected rows are visible in this slice, don't render
7291
+ if (y1 === null || y2 === null)
7292
+ return;
7293
+ // Draw selection highlight
7294
+ ctx.fillStyle = 'rgba(0, 120, 215, 0.2)';
7295
+ ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
7296
+ // Draw selection border
7297
+ ctx.strokeStyle = 'rgba(0, 120, 215, 0.8)';
7298
+ ctx.lineWidth = 2;
7299
+ ctx.setLineDash([]);
7300
+ ctx.strokeRect(x1 + 1, y1 + 1, x2 - x1 - 2, y2 - y1 - 2);
7301
+ }
7182
7302
  /**
7183
7303
  * Get rows that should be rendered for a specific page slice.
7184
7304
  * Includes header rows on continuation pages.
@@ -7345,6 +7465,8 @@ class TableObject extends BaseEmbeddedObject {
7345
7465
  }
7346
7466
  blur() {
7347
7467
  this.editing = false;
7468
+ // Clear cell range selection when table loses focus
7469
+ this.clearSelection();
7348
7470
  this.emit('blur', {});
7349
7471
  }
7350
7472
  hasFocus() {
@@ -11411,7 +11533,9 @@ class FlowingTextRenderer extends EventEmitter {
11411
11533
  const maxWidth = this.getAvailableWidthForRegion(region, pageIndex);
11412
11534
  const flowingContent = region.flowingContent;
11413
11535
  // Get cursor position for field selection highlighting
11414
- const cursorTextIndex = flowingContent.getCursorPosition();
11536
+ // Only use cursor position if the content has focus (otherwise fields stay "selected")
11537
+ const hasFocus = flowingContent.hasFocus();
11538
+ const cursorTextIndex = hasFocus ? flowingContent.getCursorPosition() : undefined;
11415
11539
  // Setup clipping if requested (useful for text boxes)
11416
11540
  if (clipToBounds) {
11417
11541
  ctx.save();
@@ -11448,7 +11572,7 @@ class FlowingTextRenderer extends EventEmitter {
11448
11572
  }
11449
11573
  // Render cursor if this region is active and cursor should be shown
11450
11574
  if (renderCursor && flowingContent.hasFocus() && flowingContent.isCursorVisible()) {
11451
- this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, cursorTextIndex);
11575
+ this.renderRegionCursor(flowedLines, ctx, bounds, maxWidth, flowingContent.getCursorPosition());
11452
11576
  }
11453
11577
  if (clipToBounds) {
11454
11578
  ctx.restore();
@@ -11556,8 +11680,10 @@ class FlowingTextRenderer extends EventEmitter {
11556
11680
  this.pageTextOffsets.set(pageIndex, 0);
11557
11681
  }
11558
11682
  // Get cursor position from the specified FlowingTextContent, or fall back to body
11683
+ // Only use cursor position for field selection if the content has focus
11559
11684
  const contentForCursor = flowingContent || this.document.bodyFlowingContent;
11560
- const cursorTextIndex = contentForCursor ? contentForCursor.getCursorPosition() : 0;
11685
+ const hasFocus = contentForCursor?.hasFocus() ?? false;
11686
+ const cursorTextIndex = hasFocus && contentForCursor ? contentForCursor.getCursorPosition() : undefined;
11561
11687
  // Get total page count for page count fields
11562
11688
  const firstPage = this.document.pages[0];
11563
11689
  const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
@@ -11639,7 +11765,7 @@ class FlowingTextRenderer extends EventEmitter {
11639
11765
  ctx.textBaseline = 'alphabetic';
11640
11766
  // Render list marker if this is the first line of a list item
11641
11767
  if (line.listMarker?.isFirstLineOfListItem && line.listMarker.text) {
11642
- this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting);
11768
+ this.renderListMarker(line.listMarker, ctx, position, line.baseline, line.runs[0]?.formatting || line.listMarker.formatting);
11643
11769
  }
11644
11770
  // Create maps for quick lookup by text index
11645
11771
  const substitutionFieldMap = new Map();
@@ -12873,11 +12999,14 @@ class FlowingTextRenderer extends EventEmitter {
12873
12999
  for (const line of flowedPage.lines) {
12874
13000
  if (this.lineContainsSelection(line, this.selectedText)) {
12875
13001
  const selectionBounds = this.getSelectionBoundsInLine(line, this.selectedText);
12876
- // Add alignment offset to position the selection correctly
12877
- const alignmentOffset = this.getAlignmentOffset(line, bounds.width);
12878
- // Account for list indentation
13002
+ // Account for list indentation - must match renderFlowedLine calculation
12879
13003
  const listIndent = line.listMarker?.indent ?? 0;
12880
- ctx.fillRect(bounds.x + alignmentOffset + listIndent + selectionBounds.x, y, selectionBounds.width, line.height);
13004
+ // Calculate alignment offset using effective width (excluding list indent)
13005
+ // This matches how renderFlowedLine calculates alignment
13006
+ const effectiveMaxWidth = bounds.width - listIndent;
13007
+ const alignmentOffset = this.getAlignmentOffset(line, effectiveMaxWidth);
13008
+ const baseX = bounds.x + listIndent;
13009
+ ctx.fillRect(baseX + alignmentOffset + selectionBounds.x, y, selectionBounds.width, line.height);
12881
13010
  }
12882
13011
  y += line.height;
12883
13012
  }
@@ -13350,15 +13479,34 @@ class FlowingTextRenderer extends EventEmitter {
13350
13479
  }
13351
13480
  /**
13352
13481
  * Get a complete snapshot of all flowed content for PDF export.
13353
- * Returns body pages, header, and footer content.
13482
+ * Returns body pages, header, footer content, and hyperlinks.
13354
13483
  */
13355
13484
  getFlowedPagesSnapshot() {
13356
13485
  const firstPage = this.document.pages[0];
13357
13486
  const bodyPages = firstPage ? this.flowedPages.get(firstPage.id) || [] : [];
13487
+ // Extract hyperlinks from each content area
13488
+ const bodyHyperlinks = this.document.bodyFlowingContent?.getAllHyperlinks().map(h => ({
13489
+ url: h.url,
13490
+ startIndex: h.startIndex,
13491
+ endIndex: h.endIndex
13492
+ }));
13493
+ const headerHyperlinks = this.document.headerFlowingContent?.getAllHyperlinks().map(h => ({
13494
+ url: h.url,
13495
+ startIndex: h.startIndex,
13496
+ endIndex: h.endIndex
13497
+ }));
13498
+ const footerHyperlinks = this.document.footerFlowingContent?.getAllHyperlinks().map(h => ({
13499
+ url: h.url,
13500
+ startIndex: h.startIndex,
13501
+ endIndex: h.endIndex
13502
+ }));
13358
13503
  return {
13359
13504
  body: bodyPages,
13360
13505
  header: this.headerFlowedPage,
13361
- footer: this.footerFlowedPage
13506
+ footer: this.footerFlowedPage,
13507
+ bodyHyperlinks: bodyHyperlinks?.length ? bodyHyperlinks : undefined,
13508
+ headerHyperlinks: headerHyperlinks?.length ? headerHyperlinks : undefined,
13509
+ footerHyperlinks: footerHyperlinks?.length ? footerHyperlinks : undefined
13362
13510
  };
13363
13511
  }
13364
13512
  // ============================================
@@ -13788,6 +13936,25 @@ class CanvasManager extends EventEmitter {
13788
13936
  this.canvases.clear();
13789
13937
  this.contexts.clear();
13790
13938
  }
13939
+ /**
13940
+ * Update canvas sizes to match current page dimensions.
13941
+ * Call this when page size or orientation changes.
13942
+ */
13943
+ updateCanvasSizes() {
13944
+ this.document.pages.forEach(page => {
13945
+ const canvas = this.canvases.get(page.id);
13946
+ if (!canvas)
13947
+ return;
13948
+ const dimensions = page.getPageDimensions();
13949
+ // Only update if dimensions have changed
13950
+ if (canvas.width !== dimensions.width || canvas.height !== dimensions.height) {
13951
+ canvas.width = dimensions.width;
13952
+ canvas.height = dimensions.height;
13953
+ }
13954
+ });
13955
+ // Update zoom scale to account for new dimensions
13956
+ this.updateCanvasScale();
13957
+ }
13791
13958
  render() {
13792
13959
  this.document.pages.forEach(page => {
13793
13960
  const ctx = this.contexts.get(page.id);
@@ -14127,6 +14294,12 @@ class CanvasManager extends EventEmitter {
14127
14294
  const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
14128
14295
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14129
14296
  const object = embeddedObjectHit.data.object;
14297
+ // Check which section the object belongs to - only interact if in active section
14298
+ const objectSection = this.getSectionForEmbeddedObject(object);
14299
+ if (objectSection && objectSection !== this._activeSection) {
14300
+ // Object is in a different section - ignore the interaction
14301
+ return;
14302
+ }
14130
14303
  // For relative-positioned objects, prepare for potential drag
14131
14304
  // Don't start drag immediately - wait for threshold to allow double-click
14132
14305
  if (object.position === 'relative') {
@@ -14295,19 +14468,40 @@ class CanvasManager extends EventEmitter {
14295
14468
  // Handle table cell selection drag
14296
14469
  if (this.isSelectingTableCells && this.tableCellSelectionStart && this.tableCellSelectionTable) {
14297
14470
  const table = this.tableCellSelectionTable;
14298
- if (table.renderedPosition) {
14299
- const localPoint = {
14300
- x: point.x - table.renderedPosition.x,
14301
- y: point.y - table.renderedPosition.y
14302
- };
14303
- const cellAddr = table.getCellAtPoint(localPoint);
14304
- if (cellAddr) {
14305
- // Update selection range
14306
- table.selectRange({
14307
- start: this.tableCellSelectionStart,
14308
- end: cellAddr
14309
- });
14310
- this.render();
14471
+ const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14472
+ // Get the slice for the current page (for multi-page tables)
14473
+ const slice = table.getRenderedSlice(currentPageIndex);
14474
+ const tablePosition = slice?.position || table.renderedPosition;
14475
+ const sliceHeight = slice?.height || table.height;
14476
+ if (tablePosition) {
14477
+ // Check if point is within the table slice on this page
14478
+ const isInsideTable = point.x >= tablePosition.x &&
14479
+ point.x <= tablePosition.x + table.width &&
14480
+ point.y >= tablePosition.y &&
14481
+ point.y <= tablePosition.y + sliceHeight;
14482
+ if (isInsideTable) {
14483
+ const localPoint = {
14484
+ x: point.x - tablePosition.x,
14485
+ y: point.y - tablePosition.y
14486
+ };
14487
+ // If this is a continuation slice, adjust y for the slice offset
14488
+ if (slice && (slice.slicePosition === 'middle' || slice.slicePosition === 'last')) {
14489
+ const headerHeight = slice.headerHeight;
14490
+ if (localPoint.y >= headerHeight) {
14491
+ // Click is in the data rows area - transform coordinates
14492
+ localPoint.y = slice.yOffset + (localPoint.y - headerHeight);
14493
+ }
14494
+ // If y < headerHeight, click is in repeated header - no adjustment needed
14495
+ }
14496
+ const cellAddr = table.getCellAtPoint(localPoint);
14497
+ if (cellAddr) {
14498
+ // Update selection range
14499
+ table.selectRange({
14500
+ start: this.tableCellSelectionStart,
14501
+ end: cellAddr
14502
+ });
14503
+ this.render();
14504
+ }
14311
14505
  }
14312
14506
  }
14313
14507
  e.preventDefault();
@@ -14416,6 +14610,10 @@ class CanvasManager extends EventEmitter {
14416
14610
  });
14417
14611
  }
14418
14612
  }
14613
+ // Re-render to update rendered positions, then update resize handle hit targets
14614
+ // This fixes the bug where resize handles don't work after a resize operation
14615
+ this.render();
14616
+ this.updateResizeHandleHitTargets();
14419
14617
  }
14420
14618
  this.isResizing = false;
14421
14619
  this.dragStart = null;
@@ -14584,13 +14782,21 @@ class CanvasManager extends EventEmitter {
14584
14782
  const hitTestManager = this.flowingTextRenderer.hitTestManager;
14585
14783
  const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
14586
14784
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14587
- // Clicked on embedded object - clear text selection and select it
14785
+ const clickedObject = embeddedObjectHit.data.object;
14786
+ // Check which section the object belongs to
14787
+ const objectSection = this.getSectionForEmbeddedObject(clickedObject);
14788
+ // Only allow selection if object is in the active section
14789
+ if (objectSection && objectSection !== this._activeSection) {
14790
+ // Object is in a different section - ignore the click
14791
+ return;
14792
+ }
14793
+ // Clicked on embedded object in the active section - clear text selection and select it
14588
14794
  const activeFlowingContent = this.getFlowingContentForActiveSection();
14589
14795
  if (activeFlowingContent) {
14590
14796
  activeFlowingContent.clearSelection();
14591
14797
  }
14592
14798
  this.clearSelection();
14593
- this.selectInlineElement(embeddedObjectHit.data.object);
14799
+ this.selectInlineElement(clickedObject);
14594
14800
  return;
14595
14801
  }
14596
14802
  // First check if we clicked on a repeating section indicator
@@ -14830,11 +15036,50 @@ class CanvasManager extends EventEmitter {
14830
15036
  const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
14831
15037
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14832
15038
  const object = embeddedObjectHit.data.object;
14833
- if (object.position === 'relative') {
14834
- canvas.style.cursor = 'move';
14835
- return;
15039
+ // Only show interactive cursors for objects in the active section
15040
+ const objectSection = this.getSectionForEmbeddedObject(object);
15041
+ if (objectSection && objectSection !== this._activeSection) ;
15042
+ else {
15043
+ if (object.position === 'relative') {
15044
+ canvas.style.cursor = 'move';
15045
+ return;
15046
+ }
15047
+ // Show text cursor for text boxes
15048
+ if (object instanceof TextBoxObject) {
15049
+ canvas.style.cursor = 'text';
15050
+ return;
15051
+ }
14836
15052
  }
14837
15053
  }
15054
+ // Check for table cells (show text cursor)
15055
+ const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15056
+ if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15057
+ canvas.style.cursor = 'text';
15058
+ return;
15059
+ }
15060
+ // Check for text regions (body, header, footer - show text cursor)
15061
+ const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15062
+ if (textRegionHit && textRegionHit.data.type === 'text-region') {
15063
+ canvas.style.cursor = 'text';
15064
+ return;
15065
+ }
15066
+ // Also check if point is within any editable region (body, header, footer)
15067
+ // This catches cases where text region hit targets may not cover empty space
15068
+ const bodyRegion = this.regionManager.getBodyRegion();
15069
+ if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15070
+ canvas.style.cursor = 'text';
15071
+ return;
15072
+ }
15073
+ const headerRegion = this.regionManager.getHeaderRegion();
15074
+ if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15075
+ canvas.style.cursor = 'text';
15076
+ return;
15077
+ }
15078
+ const footerRegion = this.regionManager.getFooterRegion();
15079
+ if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15080
+ canvas.style.cursor = 'text';
15081
+ return;
15082
+ }
14838
15083
  canvas.style.cursor = 'default';
14839
15084
  }
14840
15085
  /**
@@ -15011,6 +15256,9 @@ class CanvasManager extends EventEmitter {
15011
15256
  return this.selectedElements.size > 0;
15012
15257
  }
15013
15258
  selectBaseEmbeddedObject(embeddedObject, isPartOfRangeSelection = false) {
15259
+ // Clear focus from any currently focused control (e.g., header/footer text)
15260
+ // This ensures cursor stops blinking in the previous section
15261
+ this.setFocus(null);
15014
15262
  // Mark the embedded object as selected
15015
15263
  const obj = embeddedObject.object || embeddedObject;
15016
15264
  obj.selected = true;
@@ -15477,6 +15725,27 @@ class CanvasManager extends EventEmitter {
15477
15725
  this.emit('section-focus-changed', { section, previousSection });
15478
15726
  }
15479
15727
  }
15728
+ /**
15729
+ * Determine which section (body/header/footer) an embedded object belongs to.
15730
+ */
15731
+ getSectionForEmbeddedObject(object) {
15732
+ const sectionMappings = [
15733
+ { content: this.document.bodyFlowingContent, section: 'body' },
15734
+ { content: this.document.headerFlowingContent, section: 'header' },
15735
+ { content: this.document.footerFlowingContent, section: 'footer' }
15736
+ ];
15737
+ for (const { content, section } of sectionMappings) {
15738
+ if (!content)
15739
+ continue;
15740
+ const embeddedObjects = content.getEmbeddedObjects();
15741
+ for (const [, embeddedObj] of embeddedObjects.entries()) {
15742
+ if (embeddedObj.id === object.id) {
15743
+ return section;
15744
+ }
15745
+ }
15746
+ }
15747
+ return null;
15748
+ }
15480
15749
  /**
15481
15750
  * Detect which section a point is in based on Y coordinate.
15482
15751
  * Uses full page width areas (not just content bounds).
@@ -16090,11 +16359,11 @@ class PDFGenerator {
16090
16359
  width: headerRegion.size.width,
16091
16360
  height: headerRegion.size.height
16092
16361
  };
16093
- await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length);
16362
+ await this.renderFlowedPage(pdfPage, flowedContent.header, headerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.headerHyperlinks);
16094
16363
  }
16095
16364
  // Render body content
16096
16365
  if (flowedContent?.body && flowedContent.body[pageIndex]) {
16097
- await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length);
16366
+ await this.renderFlowedPage(pdfPage, flowedContent.body[pageIndex], contentBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.bodyHyperlinks);
16098
16367
  }
16099
16368
  // Render footer if present
16100
16369
  if (flowedContent?.footer && document.footerFlowingContent) {
@@ -16105,7 +16374,7 @@ class PDFGenerator {
16105
16374
  width: footerRegion.size.width,
16106
16375
  height: footerRegion.size.height
16107
16376
  };
16108
- await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length);
16377
+ await this.renderFlowedPage(pdfPage, flowedContent.footer, footerBounds, dimensions.height, pageIndex, document.pages.length, flowedContent.footerHyperlinks);
16109
16378
  }
16110
16379
  }
16111
16380
  catch (pageError) {
@@ -16190,10 +16459,12 @@ class PDFGenerator {
16190
16459
  /**
16191
16460
  * Render a flowed page to PDF.
16192
16461
  */
16193
- async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages) {
16462
+ async renderFlowedPage(pdfPage, flowedPage, bounds, pageHeight, pageIndex, totalPages, hyperlinks) {
16194
16463
  let y = bounds.y;
16195
16464
  // Track relative objects to render after all lines (so they appear on top)
16196
16465
  const relativeObjects = [];
16466
+ // Track rendered text positions for hyperlink annotations
16467
+ const renderedRuns = [];
16197
16468
  for (const line of flowedPage.lines) {
16198
16469
  // Collect relative objects from this line
16199
16470
  if (line.embeddedObjects) {
@@ -16224,10 +16495,10 @@ class PDFGenerator {
16224
16495
  const font = this.getFont(formatting);
16225
16496
  const fontSize = formatting.fontSize || 14;
16226
16497
  const color = parseColor(formatting.color || '#000000');
16498
+ const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16227
16499
  // Draw background if present
16228
16500
  if (formatting.backgroundColor) {
16229
16501
  const bgColor = parseColor(formatting.backgroundColor);
16230
- const textWidth = font.widthOfTextAtSize(safeText, fontSize);
16231
16502
  drawFilledRect(pdfPage, runX, y, textWidth, line.height, bgColor, pageHeight);
16232
16503
  }
16233
16504
  // Draw text - position at baseline
@@ -16239,8 +16510,19 @@ class PDFGenerator {
16239
16510
  size: fontSize,
16240
16511
  color
16241
16512
  });
16513
+ // Track this run's position for hyperlink annotations
16514
+ if (hyperlinks && hyperlinks.length > 0) {
16515
+ renderedRuns.push({
16516
+ x: runX,
16517
+ y: y,
16518
+ width: textWidth,
16519
+ height: line.height,
16520
+ startIndex: run.startIndex,
16521
+ endIndex: run.endIndex
16522
+ });
16523
+ }
16242
16524
  // Advance X position
16243
- runX += font.widthOfTextAtSize(safeText, fontSize);
16525
+ runX += textWidth;
16244
16526
  // Add extra word spacing for justified text
16245
16527
  if (line.extraWordSpacing && safeText.includes(' ')) {
16246
16528
  const spaceCount = (safeText.match(/ /g) || []).length;
@@ -16278,6 +16560,62 @@ class PDFGenerator {
16278
16560
  }
16279
16561
  // Render relative objects last (so they appear on top of text)
16280
16562
  await this.renderRelativeObjects(pdfPage, relativeObjects, pageHeight, pageIndex, totalPages);
16563
+ // Create hyperlink annotations
16564
+ if (hyperlinks && hyperlinks.length > 0 && renderedRuns.length > 0) {
16565
+ this.createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight);
16566
+ }
16567
+ }
16568
+ /**
16569
+ * Create PDF link annotations for hyperlinks.
16570
+ * Matches hyperlink ranges with rendered text positions and creates clickable links.
16571
+ */
16572
+ createHyperlinkAnnotations(pdfPage, renderedRuns, hyperlinks, pageHeight) {
16573
+ for (const hyperlink of hyperlinks) {
16574
+ // Find all runs that overlap with this hyperlink
16575
+ const overlappingRuns = renderedRuns.filter(run => run.startIndex < hyperlink.endIndex && run.endIndex > hyperlink.startIndex);
16576
+ if (overlappingRuns.length === 0)
16577
+ continue;
16578
+ // Calculate bounding box for the hyperlink
16579
+ // For multi-line hyperlinks, we create separate annotations for each line segment
16580
+ // Group runs by their Y position (same line)
16581
+ const runsByLine = new Map();
16582
+ for (const run of overlappingRuns) {
16583
+ const key = run.y;
16584
+ if (!runsByLine.has(key)) {
16585
+ runsByLine.set(key, []);
16586
+ }
16587
+ runsByLine.get(key).push(run);
16588
+ }
16589
+ // Create an annotation for each line segment
16590
+ for (const [lineY, lineRuns] of runsByLine) {
16591
+ // Calculate bounds for this line's portion of the hyperlink
16592
+ const minX = Math.min(...lineRuns.map(r => r.x));
16593
+ const maxX = Math.max(...lineRuns.map(r => r.x + r.width));
16594
+ const height = lineRuns[0].height;
16595
+ // Transform to PDF coordinates (Y is inverted)
16596
+ const pdfY = pageHeight - lineY - height;
16597
+ // Create link annotation using pdf-lib
16598
+ const linkAnnotation = pdfPage.doc.context.obj({
16599
+ Type: 'Annot',
16600
+ Subtype: 'Link',
16601
+ Rect: [minX, pdfY, maxX, pdfY + height],
16602
+ Border: [0, 0, 0], // No visible border
16603
+ A: {
16604
+ Type: 'Action',
16605
+ S: 'URI',
16606
+ URI: hyperlink.url
16607
+ }
16608
+ });
16609
+ // Add annotation to page
16610
+ const annotations = pdfPage.node.get(pdfPage.doc.context.obj('Annots'));
16611
+ if (annotations) {
16612
+ annotations.push(linkAnnotation);
16613
+ }
16614
+ else {
16615
+ pdfPage.node.set(pdfPage.doc.context.obj('Annots'), pdfPage.doc.context.obj([linkAnnotation]));
16616
+ }
16617
+ }
16618
+ }
16281
16619
  }
16282
16620
  /**
16283
16621
  * Calculate alignment offset for a line.
@@ -20903,8 +21241,12 @@ class PCEditor extends EventEmitter {
20903
21241
  }
20904
21242
  });
20905
21243
  this.document.on('settings-changed', () => {
20906
- // When settings change (like margins), we need to check if pages need to be added/removed
21244
+ // When settings change (like page size, orientation, margins), we need to:
21245
+ // 1. Update canvas sizes to match new page dimensions
21246
+ // 2. Re-render the content
21247
+ // 3. Check if pages need to be added/removed
20907
21248
  if (this.canvasManager) {
21249
+ this.canvasManager.updateCanvasSizes();
20908
21250
  this.canvasManager.render();
20909
21251
  // Defer the page check to allow reflow to complete
20910
21252
  setTimeout(() => {