@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.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.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
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
if
|
|
14326
|
-
|
|
14327
|
-
table.
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
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 +=
|
|
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
|
|
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(() => {
|