html2canvas-pro 2.3.4 → 2.3.6

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.
@@ -168,17 +168,36 @@ class DocumentCloner {
168
168
  }
169
169
  }
170
170
  if ((0, node_type_guards_1.isCustomElement)(clone) && !(0, node_type_guards_1.isSVGElementNode)(clone)) {
171
- return this.createCustomElementClone(clone);
171
+ return this.createCustomElementClone(node, clone);
172
172
  }
173
173
  return clone;
174
174
  }
175
- createCustomElementClone(node) {
175
+ createCustomElementClone(originalNode, clonedNode) {
176
176
  const clone = document.createElement('div');
177
- clone.className = node.className;
178
- (0, exports.copyCSSStyles)(node.style, clone);
179
- // Clone shadow DOM if it exists
177
+ clone.className = clonedNode.className;
178
+ (0, exports.copyCSSStyles)(clonedNode.style, clone);
179
+ // Copy all attributes onto the fresh <div> (except class/style which are
180
+ // handled above). Without this, attributes such as `slot="suffix"` are
181
+ // dropped and slotted custom elements lose their slot assignment in the
182
+ // cloned shadow DOM, so their content is never rendered (issue #226).
183
+ // Iterate by index: `NamedNodeMap` is not guaranteed to be iterable
184
+ // across TypeScript DOM lib versions (TS2488 on older libs).
185
+ const attributes = clonedNode.attributes;
186
+ for (let i = 0; i < attributes.length; i++) {
187
+ const attr = attributes[i];
188
+ if (attr.name !== 'class' && attr.name !== 'style') {
189
+ clone.setAttribute(attr.name, attr.value);
190
+ }
191
+ }
192
+ // Clone shadow DOM if it exists. The shadow root must be detected on the
193
+ // ORIGINAL element: `clonedNode` comes from `cloneNode(false)`, which only
194
+ // preserves the shadow root when it was created with `clonable: true` (or
195
+ // by frameworks that set it). For other components (e.g. Shoelace / Web
196
+ // Awesome) the clone has no shadow root, so without this check the clone
197
+ // falls back to flattening the shadow content and the slotted light-DOM
198
+ // label content is lost.
180
199
  // Fix for Issue #108: This is critical for Web Components with slots to work correctly
181
- if (node.shadowRoot) {
200
+ if (originalNode.shadowRoot) {
182
201
  try {
183
202
  clone.attachShadow({ mode: 'open' });
184
203
  // The actual shadow DOM content will be cloned in cloneChildNodes
@@ -25,6 +25,12 @@ class ElementContainer {
25
25
  this.originalElement = element;
26
26
  }
27
27
  this.bounds = (0, bounds_1.parseBounds)(this.context, element);
28
+ if (element.tagName === 'FIELDSET') {
29
+ const legend = element.querySelector(':scope > legend');
30
+ if (legend) {
31
+ this.legendBounds = (0, bounds_1.parseBounds)(this.context, legend);
32
+ }
33
+ }
28
34
  if ((0, debugger_1.isDebugging)(element, 4 /* DebuggerType.RENDER */)) {
29
35
  this.debugRender = true;
30
36
  }
@@ -18,37 +18,49 @@ const LIST_OWNERS = ['OL', 'UL', 'MENU'];
18
18
  const parseNodeTree = (context, node, parent, root) => {
19
19
  for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
20
20
  nextNode = childNode.nextSibling;
21
- // Fixes #2238 #1624 - Fix the issue of TextNode content being overlooked in rendering due to being perceived as blank by trim().
22
- if ((0, node_type_guards_1.isTextNode)(childNode) && childNode.data.length > 0) {
23
- parent.textNodes.push(new text_container_1.TextContainer(context, childNode, parent.styles));
21
+ parseChildNode(context, childNode, parent, root);
22
+ }
23
+ };
24
+ /**
25
+ * Parse a single node as a child of `parent`.
26
+ *
27
+ * Separated from the tree walk so that nodes assigned to a `<slot>` (including
28
+ * bare text nodes, which have no child nodes) can be parsed directly. Without
29
+ * this, slotted shadow-DOM content such as web component labels was silently
30
+ * dropped because `parseNodeTree` only iterates `node.firstChild` (issue #226).
31
+ */
32
+ const parseChildNode = (context, childNode, parent, root) => {
33
+ // Fixes #2238 #1624 - Fix the issue of TextNode content being overlooked in rendering due to being perceived as blank by trim().
34
+ if ((0, node_type_guards_1.isTextNode)(childNode) && childNode.data.length > 0) {
35
+ parent.textNodes.push(new text_container_1.TextContainer(context, childNode, parent.styles));
36
+ }
37
+ else if ((0, node_type_guards_1.isElementNode)(childNode)) {
38
+ if ((0, node_type_guards_1.isSlotElement)(childNode) && childNode.assignedNodes) {
39
+ // Slotted content is laid out at the slot's position: parse each
40
+ // assigned node (text or element) as a direct child of the slot's
41
+ // parent container.
42
+ childNode
43
+ .assignedNodes()
44
+ .forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root));
24
45
  }
25
- else if ((0, node_type_guards_1.isElementNode)(childNode)) {
26
- if ((0, node_type_guards_1.isSlotElement)(childNode) && childNode.assignedNodes) {
27
- childNode
28
- .assignedNodes()
29
- .forEach((assignedNode) => parseNodeTree(context, assignedNode, parent, root));
30
- }
31
- else {
32
- const container = createContainer(context, childNode);
33
- if (container.styles.isVisible()) {
34
- if (createsRealStackingContext(childNode, container, root)) {
35
- container.createsRealStackingContext = true;
36
- }
37
- else if (createsStackingContext(container.styles)) {
38
- container.createsStackingContext = true;
39
- }
40
- if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
41
- container.isListOwner = true;
42
- }
43
- parent.elements.push(container);
44
- if (childNode.shadowRoot) {
45
- parseNodeTree(context, childNode.shadowRoot, container, root);
46
- }
47
- else if (!(0, node_type_guards_1.isTextareaElement)(childNode) &&
48
- !(0, node_type_guards_1.isSVGElement)(childNode) &&
49
- !(0, node_type_guards_1.isSelectElement)(childNode)) {
50
- parseNodeTree(context, childNode, container, root);
51
- }
46
+ else {
47
+ const container = createContainer(context, childNode);
48
+ if (container.styles.isVisible()) {
49
+ if (createsRealStackingContext(childNode, container, root)) {
50
+ container.createsRealStackingContext = true;
51
+ }
52
+ else if (createsStackingContext(container.styles)) {
53
+ container.createsStackingContext = true;
54
+ }
55
+ if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
56
+ container.isListOwner = true;
57
+ }
58
+ parent.elements.push(container);
59
+ if (childNode.shadowRoot) {
60
+ parseNodeTree(context, childNode.shadowRoot, container, root);
61
+ }
62
+ else if (!(0, node_type_guards_1.isTextareaElement)(childNode) && !(0, node_type_guards_1.isSVGElement)(childNode) && !(0, node_type_guards_1.isSelectElement)(childNode)) {
63
+ parseNodeTree(context, childNode, container, root);
52
64
  }
53
65
  }
54
66
  }
@@ -282,17 +282,50 @@ class CanvasRenderer {
282
282
  let side = 0;
283
283
  for (const border of borders) {
284
284
  if (border.style !== 0 /* BORDER_STYLE.NONE */ && !(0, color_utilities_1.isTransparent)(border.color) && border.width > 0) {
285
- if (border.style === 2 /* BORDER_STYLE.DASHED */) {
286
- await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
287
- }
288
- else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
289
- await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
290
- }
291
- else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
292
- await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
285
+ const renderBorderSide = async () => {
286
+ if (border.style === 2 /* BORDER_STYLE.DASHED */) {
287
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
288
+ }
289
+ else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
290
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
291
+ }
292
+ else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
293
+ await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
294
+ }
295
+ else {
296
+ await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
297
+ }
298
+ };
299
+ // Browsers break a <fieldset>'s top border where its <legend>
300
+ // sits, instead of drawing the border through the legend (issue
301
+ // #227). Render the top border in two clipped segments around
302
+ // the legend's horizontal span.
303
+ const legend = paint.container.legendBounds;
304
+ if (side === 0 && legend) {
305
+ const bounds = paint.container.bounds;
306
+ const legendLeft = legend.left - bounds.left;
307
+ const legendRight = legend.left + legend.width - bounds.left;
308
+ if (legendLeft > 0) {
309
+ this.ctx.save();
310
+ this.ctx.beginPath();
311
+ this.ctx.rect(bounds.left, bounds.top, legendLeft, bounds.height);
312
+ this.ctx.clip();
313
+ await renderBorderSide();
314
+ this.ctx.restore();
315
+ }
316
+ const rightStart = bounds.left + legendRight;
317
+ const rightWidth = bounds.width - legendRight;
318
+ if (rightWidth > 0) {
319
+ this.ctx.save();
320
+ this.ctx.beginPath();
321
+ this.ctx.rect(rightStart, bounds.top, rightWidth, bounds.height);
322
+ this.ctx.clip();
323
+ await renderBorderSide();
324
+ this.ctx.restore();
325
+ }
293
326
  }
294
327
  else {
295
- await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
328
+ await renderBorderSide();
296
329
  }
297
330
  }
298
331
  side++;
@@ -21,6 +21,10 @@ const text_decoration_renderer_1 = require("./text/text-decoration-renderer");
21
21
  const font_utils_1 = require("./font-utils");
22
22
  // iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
23
23
  const iOSBrokenFonts = ['-apple-system', 'system-ui'];
24
+ // Tolerance (px) for text-overflow: ellipsis detection. Canvas measureText can
25
+ // report a width a fraction of a pixel wider than the browser's layout box, so
26
+ // text that fits exactly must not be truncated (issue #226).
27
+ const TEXT_OVERFLOW_EPSILON = 1;
24
28
  /**
25
29
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
26
30
  * CJK characters use the ideographic baseline in browsers, which differs
@@ -424,7 +428,10 @@ class TextRenderer {
424
428
  .replace(/\s+/g, ' ')
425
429
  .trim();
426
430
  const fullWidth = this.ctx.measureText(fullText).width;
427
- if (fullWidth <= containerBounds.width)
431
+ // Small tolerance: canvas measurement can differ from the browser's text
432
+ // layout by a fraction of a pixel (kerning/font rounding), so text that
433
+ // exactly fits the container must not be falsely truncated (issue #226).
434
+ if (fullWidth <= containerBounds.width + TEXT_OVERFLOW_EPSILON)
428
435
  return false;
429
436
  const truncated = this.truncateTextWithEllipsis(fullText, containerBounds.width, styles.letterSpacing);
430
437
  const bounds = new text_1.TextBounds(truncated, text.textBounds[0].bounds);
@@ -30,7 +30,7 @@ export declare class DocumentCloner {
30
30
  constructor(context: Context, element: HTMLElement, options: CloneConfigurations);
31
31
  toIFrame(ownerDocument: Document, windowSize: Bounds): Promise<HTMLIFrameElement>;
32
32
  createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement;
33
- createCustomElementClone(node: HTMLElement): HTMLElement;
33
+ createCustomElementClone(originalNode: HTMLElement, clonedNode: HTMLElement): HTMLElement;
34
34
  createStyleClone(node: HTMLStyleElement): HTMLStyleElement;
35
35
  createCanvasClone(canvas: HTMLCanvasElement): HTMLImageElement | HTMLCanvasElement;
36
36
  createVideoClone(video: HTMLVideoElement): HTMLCanvasElement;
@@ -11,6 +11,12 @@ export declare class ElementContainer {
11
11
  readonly textNodes: TextContainer[];
12
12
  readonly elements: ElementContainer[];
13
13
  bounds: Bounds;
14
+ /**
15
+ * Bounds of the <legend> child of a <fieldset>, when present. Browsers break
16
+ * the fieldset's top border where the legend sits; the renderer uses these
17
+ * bounds to leave a matching gap (issue #227).
18
+ */
19
+ readonly legendBounds?: Bounds;
14
20
  createsStackingContext: boolean;
15
21
  createsRealStackingContext: boolean;
16
22
  isListOwner: boolean;
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "default": "./dist/html2canvas-pro.esm.js"
20
20
  }
21
21
  },
22
- "version": "2.3.4",
22
+ "version": "2.3.6",
23
23
  "author": {
24
24
  "name": "yorickshan",
25
25
  "email": "yorickshan@gmail.com",