@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.
- package/dist/pc-editor.esm.js +380 -38
- package/dist/pc-editor.esm.js.map +1 -1
- package/dist/pc-editor.js +380 -38
- package/dist/pc-editor.js.map +1 -1
- package/dist/pc-editor.min.js +1 -1
- package/dist/pc-editor.min.js.map +1 -1
- package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
- package/dist/types/lib/objects/ImageObject.d.ts.map +1 -1
- package/dist/types/lib/objects/table/TableObject.d.ts +5 -0
- package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
- package/dist/types/lib/rendering/CanvasManager.d.ts +9 -0
- package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +16 -1
- package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
- package/dist/types/lib/rendering/PDFGenerator.d.ts +16 -0
- package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
- package/dist/types/lib/text/TextLayout.d.ts.map +1 -1
- package/dist/types/lib/text/types.d.ts +1 -0
- package/dist/types/lib/text/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/pc-editor.esm.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
if
|
|
14305
|
-
|
|
14306
|
-
table.
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
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 +=
|
|
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
|
|
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(() => {
|