html2canvas-pro 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html2canvas-pro.esm.js +307 -23
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +307 -23
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +4 -4
- package/dist/lib/css/index.js +2 -0
- package/dist/lib/css/index.js.map +1 -1
- package/dist/lib/css/property-descriptors/webkit-line-clamp.js +27 -0
- package/dist/lib/css/property-descriptors/webkit-line-clamp.js.map +1 -0
- package/dist/lib/dom/document-cloner.js +149 -8
- package/dist/lib/dom/document-cloner.js.map +1 -1
- package/dist/lib/render/canvas/canvas-renderer.js +132 -14
- package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
- package/dist/types/css/index.d.ts +2 -0
- package/dist/types/css/property-descriptors/webkit-line-clamp.d.ts +7 -0
- package/dist/types/dom/document-cloner.d.ts +46 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 1.6.
|
|
2
|
+
* html2canvas-pro 1.6.7 <https://yorickshan.github.io/html2canvas-pro/>
|
|
3
3
|
* Copyright (c) 2024-present yorickshan and html2canvas-pro contributors
|
|
4
4
|
* Released under MIT License
|
|
5
5
|
*/
|
|
@@ -4488,6 +4488,30 @@ const webkitTextStrokeWidth = {
|
|
|
4488
4488
|
}
|
|
4489
4489
|
};
|
|
4490
4490
|
|
|
4491
|
+
/**
|
|
4492
|
+
* -webkit-line-clamp property descriptor
|
|
4493
|
+
* Used with display: -webkit-box and -webkit-box-orient: vertical
|
|
4494
|
+
* to limit text to a specific number of lines
|
|
4495
|
+
*/
|
|
4496
|
+
const webkitLineClamp = {
|
|
4497
|
+
name: '-webkit-line-clamp',
|
|
4498
|
+
initialValue: 'none',
|
|
4499
|
+
prefix: true,
|
|
4500
|
+
type: 0 /* PropertyDescriptorParsingType.VALUE */,
|
|
4501
|
+
parse: (_context, token) => {
|
|
4502
|
+
// 'none' means no line clamping
|
|
4503
|
+
if (token.type === 20 /* TokenType.IDENT_TOKEN */ && token.value === 'none') {
|
|
4504
|
+
return 0;
|
|
4505
|
+
}
|
|
4506
|
+
// Number value specifies the number of lines
|
|
4507
|
+
if (token.type === 17 /* TokenType.NUMBER_TOKEN */) {
|
|
4508
|
+
return Math.max(0, Math.floor(token.number));
|
|
4509
|
+
}
|
|
4510
|
+
// Default to 0 (no clamping)
|
|
4511
|
+
return 0;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
|
|
4491
4515
|
const objectFit = {
|
|
4492
4516
|
name: 'objectFit',
|
|
4493
4517
|
initialValue: 'fill',
|
|
@@ -4601,6 +4625,7 @@ class CSSParsedDeclaration {
|
|
|
4601
4625
|
this.visibility = parse(context, visibility, declaration.visibility);
|
|
4602
4626
|
this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
|
|
4603
4627
|
this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
|
|
4628
|
+
this.webkitLineClamp = parse(context, webkitLineClamp, declaration.webkitLineClamp);
|
|
4604
4629
|
this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
|
|
4605
4630
|
this.zIndex = parse(context, zIndex, declaration.zIndex);
|
|
4606
4631
|
this.objectFit = parse(context, objectFit, declaration.objectFit);
|
|
@@ -6357,20 +6382,161 @@ class DocumentCloner {
|
|
|
6357
6382
|
}
|
|
6358
6383
|
}
|
|
6359
6384
|
}
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6385
|
+
/**
|
|
6386
|
+
* Check if a child node should be cloned based on filtering rules
|
|
6387
|
+
* Filters out: scripts, ignored elements, and optionally styles
|
|
6388
|
+
*/
|
|
6389
|
+
shouldCloneChild(child) {
|
|
6390
|
+
return (!isElementNode(child) ||
|
|
6391
|
+
(!isScriptElement(child) &&
|
|
6392
|
+
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
|
|
6393
|
+
(typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))));
|
|
6394
|
+
}
|
|
6395
|
+
/**
|
|
6396
|
+
* Check if a style element should be cloned based on copyStyles option
|
|
6397
|
+
*/
|
|
6398
|
+
shouldCloneStyleElement(child) {
|
|
6399
|
+
return !this.options.copyStyles || !isElementNode(child) || !isStyleElement(child);
|
|
6400
|
+
}
|
|
6401
|
+
/**
|
|
6402
|
+
* Safely append a cloned child to a target, applying all filtering rules
|
|
6403
|
+
*/
|
|
6404
|
+
safeAppendClonedChild(target, child, copyStyles) {
|
|
6405
|
+
if (this.shouldCloneChild(child) && this.shouldCloneStyleElement(child)) {
|
|
6406
|
+
target.appendChild(this.cloneNode(child, copyStyles));
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Clone assigned nodes from a slot element to the target
|
|
6411
|
+
*/
|
|
6412
|
+
cloneAssignedNodes(assignedNodes, target, copyStyles) {
|
|
6413
|
+
assignedNodes.forEach((node) => {
|
|
6414
|
+
this.safeAppendClonedChild(target, node, copyStyles);
|
|
6415
|
+
});
|
|
6416
|
+
}
|
|
6417
|
+
/**
|
|
6418
|
+
* Clone fallback content from a slot element when no nodes are assigned
|
|
6419
|
+
*/
|
|
6420
|
+
cloneSlotFallbackContent(slot, target, copyStyles) {
|
|
6421
|
+
for (let child = slot.firstChild; child; child = child.nextSibling) {
|
|
6422
|
+
this.safeAppendClonedChild(target, child, copyStyles);
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
/**
|
|
6426
|
+
* Handle cloning of a slot element, including assigned nodes or fallback content
|
|
6427
|
+
*/
|
|
6428
|
+
cloneSlotElement(slot, targetShadowRoot, copyStyles) {
|
|
6429
|
+
if (!isSlotElement(slot)) {
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
const slotElement = slot;
|
|
6433
|
+
// Defensive check: ensure assignedNodes method exists
|
|
6434
|
+
if (typeof slotElement.assignedNodes !== 'function') {
|
|
6435
|
+
this.context.logger.warn('HTMLSlotElement.assignedNodes is not available', slot);
|
|
6436
|
+
this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
|
|
6437
|
+
return;
|
|
6438
|
+
}
|
|
6439
|
+
const assignedNodes = slotElement.assignedNodes();
|
|
6440
|
+
// Defensive check: ensure assignedNodes returns an array
|
|
6441
|
+
if (!assignedNodes || !Array.isArray(assignedNodes)) {
|
|
6442
|
+
this.context.logger.warn('assignedNodes() did not return a valid array', slot);
|
|
6443
|
+
this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
|
|
6444
|
+
return;
|
|
6445
|
+
}
|
|
6446
|
+
if (assignedNodes.length > 0) {
|
|
6447
|
+
// Clone assigned nodes
|
|
6448
|
+
this.cloneAssignedNodes(assignedNodes, targetShadowRoot, copyStyles);
|
|
6449
|
+
}
|
|
6450
|
+
else {
|
|
6451
|
+
// Clone fallback content
|
|
6452
|
+
this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
/**
|
|
6456
|
+
* Clone shadow DOM children to the target shadow root
|
|
6457
|
+
*/
|
|
6458
|
+
cloneShadowDOMChildren(shadowRoot, targetShadowRoot, copyStyles) {
|
|
6459
|
+
for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
|
|
6460
|
+
if (isElementNode(child) && isSlotElement(child)) {
|
|
6461
|
+
// Handle slot elements specially
|
|
6462
|
+
this.cloneSlotElement(child, targetShadowRoot, copyStyles);
|
|
6463
|
+
}
|
|
6464
|
+
else {
|
|
6465
|
+
// Clone regular elements
|
|
6466
|
+
this.safeAppendClonedChild(targetShadowRoot, child, copyStyles);
|
|
6367
6467
|
}
|
|
6368
6468
|
}
|
|
6369
|
-
|
|
6469
|
+
}
|
|
6470
|
+
/**
|
|
6471
|
+
* Clone light DOM children to the target element
|
|
6472
|
+
*/
|
|
6473
|
+
cloneLightDOMChildren(node, clone, copyStyles) {
|
|
6370
6474
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
6371
6475
|
this.appendChildNode(clone, child, copyStyles);
|
|
6372
6476
|
}
|
|
6373
6477
|
}
|
|
6478
|
+
/**
|
|
6479
|
+
* Clone slot element as light DOM when shadow root creation failed
|
|
6480
|
+
*/
|
|
6481
|
+
cloneSlotElementAsLightDOM(slot, clone, copyStyles) {
|
|
6482
|
+
if (!isSlotElement(slot)) {
|
|
6483
|
+
return;
|
|
6484
|
+
}
|
|
6485
|
+
const slotElement = slot;
|
|
6486
|
+
if (typeof slotElement.assignedNodes !== 'function') {
|
|
6487
|
+
// Fallback: clone slot's children
|
|
6488
|
+
for (let child = slot.firstChild; child; child = child.nextSibling) {
|
|
6489
|
+
this.appendChildNode(clone, child, copyStyles);
|
|
6490
|
+
}
|
|
6491
|
+
return;
|
|
6492
|
+
}
|
|
6493
|
+
const assignedNodes = slotElement.assignedNodes();
|
|
6494
|
+
if (assignedNodes && Array.isArray(assignedNodes) && assignedNodes.length > 0) {
|
|
6495
|
+
// Clone assigned nodes as light DOM
|
|
6496
|
+
assignedNodes.forEach((node) => this.appendChildNode(clone, node, copyStyles));
|
|
6497
|
+
}
|
|
6498
|
+
else {
|
|
6499
|
+
// Clone fallback content as light DOM
|
|
6500
|
+
for (let child = slot.firstChild; child; child = child.nextSibling) {
|
|
6501
|
+
this.appendChildNode(clone, child, copyStyles);
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
/**
|
|
6506
|
+
* Clone shadow DOM content as light DOM when shadow root creation failed
|
|
6507
|
+
* This is a fallback mechanism to ensure content is not lost
|
|
6508
|
+
*/
|
|
6509
|
+
cloneShadowDOMAsLightDOM(shadowRoot, clone, copyStyles) {
|
|
6510
|
+
for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
|
|
6511
|
+
if (isElementNode(child) && isSlotElement(child)) {
|
|
6512
|
+
this.cloneSlotElementAsLightDOM(child, clone, copyStyles);
|
|
6513
|
+
}
|
|
6514
|
+
else {
|
|
6515
|
+
this.appendChildNode(clone, child, copyStyles);
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
/**
|
|
6520
|
+
* Clone child nodes from source element to clone element
|
|
6521
|
+
* Handles shadow DOM, slots, and light DOM appropriately
|
|
6522
|
+
*/
|
|
6523
|
+
cloneChildNodes(node, clone, copyStyles) {
|
|
6524
|
+
if (node.shadowRoot && clone.shadowRoot) {
|
|
6525
|
+
// Both original and clone have shadow roots - clone shadow DOM content
|
|
6526
|
+
this.cloneShadowDOMChildren(node.shadowRoot, clone.shadowRoot, copyStyles);
|
|
6527
|
+
// Also clone light DOM (slot content sources)
|
|
6528
|
+
this.cloneLightDOMChildren(node, clone, copyStyles);
|
|
6529
|
+
}
|
|
6530
|
+
else if (node.shadowRoot && !clone.shadowRoot) {
|
|
6531
|
+
// Original has shadow root but clone doesn't (creation failed)
|
|
6532
|
+
// Fallback: clone shadow DOM content as light DOM to preserve content
|
|
6533
|
+
this.cloneShadowDOMAsLightDOM(node.shadowRoot, clone, copyStyles);
|
|
6534
|
+
}
|
|
6535
|
+
else {
|
|
6536
|
+
// No shadow DOM - just clone light DOM children
|
|
6537
|
+
this.cloneLightDOMChildren(node, clone, copyStyles);
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6374
6540
|
cloneNode(node, copyStyles) {
|
|
6375
6541
|
if (isTextNode(node)) {
|
|
6376
6542
|
return document.createTextNode(node.data);
|
|
@@ -7638,6 +7804,28 @@ class CanvasRenderer extends Renderer {
|
|
|
7638
7804
|
}, text.bounds.left);
|
|
7639
7805
|
}
|
|
7640
7806
|
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Helper method to render text with paint order support
|
|
7809
|
+
* Reduces code duplication in line-clamp and normal rendering
|
|
7810
|
+
*/
|
|
7811
|
+
renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
|
|
7812
|
+
paintOrderLayers.forEach((paintOrderLayer) => {
|
|
7813
|
+
switch (paintOrderLayer) {
|
|
7814
|
+
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
7815
|
+
this.ctx.fillStyle = asString(styles.color);
|
|
7816
|
+
this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
|
|
7817
|
+
break;
|
|
7818
|
+
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
7819
|
+
if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
|
|
7820
|
+
this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
|
7821
|
+
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
7822
|
+
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
7823
|
+
this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
|
|
7824
|
+
}
|
|
7825
|
+
break;
|
|
7826
|
+
}
|
|
7827
|
+
});
|
|
7828
|
+
}
|
|
7641
7829
|
renderTextDecoration(bounds, styles) {
|
|
7642
7830
|
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
|
7643
7831
|
// Calculate decoration line thickness
|
|
@@ -7787,24 +7975,120 @@ class CanvasRenderer extends Renderer {
|
|
|
7787
7975
|
this.ctx.textAlign = 'left';
|
|
7788
7976
|
this.ctx.textBaseline = 'alphabetic';
|
|
7789
7977
|
const paintOrder = styles.paintOrder;
|
|
7978
|
+
// Calculate line height for text layout detection (used by both line-clamp and ellipsis)
|
|
7979
|
+
const lineHeight = styles.fontSize.number * 1.5;
|
|
7980
|
+
// Check if we need to apply -webkit-line-clamp
|
|
7981
|
+
// This limits text to a specific number of lines with ellipsis
|
|
7982
|
+
const shouldApplyLineClamp = styles.webkitLineClamp > 0 &&
|
|
7983
|
+
(styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
|
|
7984
|
+
styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
|
|
7985
|
+
text.textBounds.length > 0;
|
|
7986
|
+
if (shouldApplyLineClamp) {
|
|
7987
|
+
// Group text bounds by lines based on their Y position
|
|
7988
|
+
const lines = [];
|
|
7989
|
+
let currentLine = [];
|
|
7990
|
+
let currentLineTop = text.textBounds[0].bounds.top;
|
|
7991
|
+
text.textBounds.forEach((tb) => {
|
|
7992
|
+
// If this text bound is on a different line, start a new line
|
|
7993
|
+
if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
|
|
7994
|
+
if (currentLine.length > 0) {
|
|
7995
|
+
lines.push(currentLine);
|
|
7996
|
+
}
|
|
7997
|
+
currentLine = [tb];
|
|
7998
|
+
currentLineTop = tb.bounds.top;
|
|
7999
|
+
}
|
|
8000
|
+
else {
|
|
8001
|
+
currentLine.push(tb);
|
|
8002
|
+
}
|
|
8003
|
+
});
|
|
8004
|
+
// Don't forget the last line
|
|
8005
|
+
if (currentLine.length > 0) {
|
|
8006
|
+
lines.push(currentLine);
|
|
8007
|
+
}
|
|
8008
|
+
// Only render up to webkitLineClamp lines
|
|
8009
|
+
const maxLines = styles.webkitLineClamp;
|
|
8010
|
+
if (lines.length > maxLines) {
|
|
8011
|
+
// Render only the first (maxLines - 1) complete lines
|
|
8012
|
+
for (let i = 0; i < maxLines - 1; i++) {
|
|
8013
|
+
lines[i].forEach((textBound) => {
|
|
8014
|
+
this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
8017
|
+
// For the last line, truncate with ellipsis
|
|
8018
|
+
const lastLine = lines[maxLines - 1];
|
|
8019
|
+
if (lastLine && lastLine.length > 0 && containerBounds) {
|
|
8020
|
+
const lastLineText = lastLine.map((tb) => tb.text).join('');
|
|
8021
|
+
const firstBound = lastLine[0];
|
|
8022
|
+
const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
|
|
8023
|
+
const truncatedText = this.truncateTextWithEllipsis(lastLineText, availableWidth, styles.letterSpacing);
|
|
8024
|
+
paintOrder.forEach((paintOrderLayer) => {
|
|
8025
|
+
switch (paintOrderLayer) {
|
|
8026
|
+
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
8027
|
+
this.ctx.fillStyle = asString(styles.color);
|
|
8028
|
+
if (styles.letterSpacing === 0) {
|
|
8029
|
+
this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
8030
|
+
}
|
|
8031
|
+
else {
|
|
8032
|
+
const letters = segmentGraphemes(truncatedText);
|
|
8033
|
+
letters.reduce((left, letter) => {
|
|
8034
|
+
this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
8035
|
+
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
8036
|
+
}, firstBound.bounds.left);
|
|
8037
|
+
}
|
|
8038
|
+
break;
|
|
8039
|
+
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
8040
|
+
if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
|
|
8041
|
+
this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
|
8042
|
+
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
8043
|
+
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
8044
|
+
if (styles.letterSpacing === 0) {
|
|
8045
|
+
this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
8046
|
+
}
|
|
8047
|
+
else {
|
|
8048
|
+
const letters = segmentGraphemes(truncatedText);
|
|
8049
|
+
letters.reduce((left, letter) => {
|
|
8050
|
+
this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
8051
|
+
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
8052
|
+
}, firstBound.bounds.left);
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
break;
|
|
8056
|
+
}
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
return; // Don't render anything else
|
|
8060
|
+
}
|
|
8061
|
+
// If lines.length <= maxLines, fall through to normal rendering
|
|
8062
|
+
}
|
|
7790
8063
|
// Check if we need to apply text-overflow: ellipsis
|
|
7791
|
-
|
|
8064
|
+
// Issue #203: Only apply ellipsis for single-line text overflow
|
|
8065
|
+
// Multi-line text truncation (like -webkit-line-clamp) should not be affected
|
|
8066
|
+
const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
|
|
8067
|
+
containerBounds &&
|
|
8068
|
+
styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
|
|
8069
|
+
text.textBounds.length > 0;
|
|
7792
8070
|
// Calculate total text width if ellipsis might be needed
|
|
7793
8071
|
let needsEllipsis = false;
|
|
7794
8072
|
let truncatedText = '';
|
|
7795
|
-
if (shouldApplyEllipsis
|
|
7796
|
-
//
|
|
7797
|
-
//
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
8073
|
+
if (shouldApplyEllipsis) {
|
|
8074
|
+
// Check if all text bounds are on approximately the same line (single-line scenario)
|
|
8075
|
+
// For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
|
|
8076
|
+
const firstTop = text.textBounds[0].bounds.top;
|
|
8077
|
+
const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
|
|
8078
|
+
if (isSingleLine) {
|
|
8079
|
+
// Measure the full text content
|
|
8080
|
+
// Note: text.textBounds may contain whitespace characters from HTML formatting
|
|
8081
|
+
// We need to collapse them like the browser does for white-space: nowrap
|
|
8082
|
+
let fullText = text.textBounds.map((tb) => tb.text).join('');
|
|
8083
|
+
// Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
|
|
8084
|
+
// and trim leading/trailing whitespace
|
|
8085
|
+
fullText = fullText.replace(/\s+/g, ' ').trim();
|
|
8086
|
+
const fullTextWidth = this.ctx.measureText(fullText).width;
|
|
8087
|
+
const availableWidth = containerBounds.width;
|
|
8088
|
+
if (fullTextWidth > availableWidth) {
|
|
8089
|
+
needsEllipsis = true;
|
|
8090
|
+
truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
|
|
8091
|
+
}
|
|
7808
8092
|
}
|
|
7809
8093
|
}
|
|
7810
8094
|
// If ellipsis is needed, render the truncated text once
|