mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Clone document before parsing it
This commit is contained in:
parent
7a3bad2fcb
commit
478155af64
@ -49,10 +49,8 @@ export const parseBounds = (node: HTMLElement, isTransformed: boolean): Bounds =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const offsetBounds = (node: HTMLElement): Bounds => {
|
const offsetBounds = (node: HTMLElement): Bounds => {
|
||||||
const parent =
|
// //$FlowFixMe
|
||||||
node.offsetParent instanceof HTMLElement
|
const parent = node.offsetParent ? offsetBounds(node.offsetParent) : {top: 0, left: 0};
|
||||||
? offsetBounds(node.offsetParent)
|
|
||||||
: {top: 0, left: 0};
|
|
||||||
|
|
||||||
return new Bounds(
|
return new Bounds(
|
||||||
node.offsetLeft + parent.left,
|
node.offsetLeft + parent.left,
|
||||||
|
160
src/Clone.js
Normal file
160
src/Clone.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/* @flow */
|
||||||
|
'use strict';
|
||||||
|
import type {Bounds} from './Bounds';
|
||||||
|
import type {Options} from './index';
|
||||||
|
|
||||||
|
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
|
||||||
|
if (
|
||||||
|
ownerDocument.defaultView &&
|
||||||
|
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
|
||||||
|
) {
|
||||||
|
ownerDocument.defaultView.scrollTo(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
|
||||||
|
try {
|
||||||
|
if (clonedCanvas) {
|
||||||
|
clonedCanvas.width = canvas.width;
|
||||||
|
clonedCanvas.height = canvas.height;
|
||||||
|
clonedCanvas
|
||||||
|
.getContext('2d')
|
||||||
|
.putImageData(
|
||||||
|
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneNode = (
|
||||||
|
node: Node,
|
||||||
|
referenceElement: [HTMLElement, ?HTMLElement],
|
||||||
|
scrolledElements: Array<[HTMLElement, number, number]>
|
||||||
|
) => {
|
||||||
|
const clone =
|
||||||
|
node.nodeType === Node.TEXT_NODE
|
||||||
|
? document.createTextNode(node.nodeValue)
|
||||||
|
: node.cloneNode(false);
|
||||||
|
|
||||||
|
if (referenceElement[0] === node && clone instanceof HTMLElement) {
|
||||||
|
referenceElement[1] = clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
|
||||||
|
clone.appendChild(cloneNode(child, referenceElement, scrolledElements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||||
|
scrolledElements.push([node, node.scrollLeft, node.scrollTop]);
|
||||||
|
}
|
||||||
|
switch (node.nodeName) {
|
||||||
|
case 'CANVAS':
|
||||||
|
// $FlowFixMe
|
||||||
|
cloneCanvasContents(node, clone);
|
||||||
|
break;
|
||||||
|
case 'TEXTAREA':
|
||||||
|
case 'SELECT':
|
||||||
|
// $FlowFixMe
|
||||||
|
clone.value = node.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
|
||||||
|
element.scrollLeft = x;
|
||||||
|
element.scrollTop = y;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloneWindow = (
|
||||||
|
documentToBeCloned: Document,
|
||||||
|
ownerDocument: Document,
|
||||||
|
bounds: Bounds,
|
||||||
|
referenceElement: HTMLElement,
|
||||||
|
options: Options
|
||||||
|
): Promise<[HTMLIFrameElement, HTMLElement]> => {
|
||||||
|
const scrolledElements = [];
|
||||||
|
const referenceElementSearch = [referenceElement, null];
|
||||||
|
if (!(documentToBeCloned.documentElement instanceof HTMLElement)) {
|
||||||
|
return Promise.reject(__DEV__ ? `Invalid document provided for cloning` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentElement = cloneNode(
|
||||||
|
documentToBeCloned.documentElement,
|
||||||
|
referenceElementSearch,
|
||||||
|
scrolledElements
|
||||||
|
);
|
||||||
|
|
||||||
|
const cloneIframeContainer = ownerDocument.createElement('iframe');
|
||||||
|
|
||||||
|
cloneIframeContainer.className = 'html2canvas-container';
|
||||||
|
cloneIframeContainer.style.visibility = 'hidden';
|
||||||
|
cloneIframeContainer.style.position = 'fixed';
|
||||||
|
cloneIframeContainer.style.left = '-10000px';
|
||||||
|
cloneIframeContainer.style.top = '0px';
|
||||||
|
cloneIframeContainer.style.border = '0';
|
||||||
|
cloneIframeContainer.width = bounds.width.toString();
|
||||||
|
cloneIframeContainer.height = bounds.height.toString();
|
||||||
|
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
|
||||||
|
if (ownerDocument.body) {
|
||||||
|
ownerDocument.body.appendChild(cloneIframeContainer);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(
|
||||||
|
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const documentClone = cloneIframeContainer.contentWindow.document;
|
||||||
|
|
||||||
|
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
|
||||||
|
if window url is about:blank, we can assign the url to current by writing onto the document
|
||||||
|
*/
|
||||||
|
cloneIframeContainer.contentWindow.onload = cloneIframeContainer.onload = () => {
|
||||||
|
console.log('iframe load');
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (documentClone.body.childNodes.length > 0) {
|
||||||
|
scrolledElements.forEach(initNode);
|
||||||
|
clearInterval(interval);
|
||||||
|
if (options.type === 'view') {
|
||||||
|
cloneIframeContainer.contentWindow.scrollTo(bounds.left, bounds.top);
|
||||||
|
if (
|
||||||
|
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
||||||
|
(cloneIframeContainer.contentWindow.scrollY !== bounds.top ||
|
||||||
|
cloneIframeContainer.contentWindow.scrollX !== bounds.left)
|
||||||
|
) {
|
||||||
|
documentClone.documentElement.style.top = -bounds.top + 'px';
|
||||||
|
documentClone.documentElement.style.left = -bounds.left + 'px';
|
||||||
|
documentClone.documentElement.style.position = 'absolute';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (referenceElementSearch[1] instanceof HTMLElement) {
|
||||||
|
resolve([cloneIframeContainer, referenceElementSearch[1]]);
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
__DEV__
|
||||||
|
? `Error finding the ${referenceElement.nodeName} in the cloned document`
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
documentClone.open();
|
||||||
|
documentClone.write('<!DOCTYPE html><html></html>');
|
||||||
|
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
||||||
|
restoreOwnerScroll(documentToBeCloned, bounds.left, bounds.top);
|
||||||
|
documentClone.replaceChild(
|
||||||
|
documentClone.adoptNode(documentElement),
|
||||||
|
documentClone.documentElement
|
||||||
|
);
|
||||||
|
documentClone.close();
|
||||||
|
});
|
||||||
|
};
|
@ -87,10 +87,10 @@ export default class NodeContainer {
|
|||||||
transform: parseTransform(style),
|
transform: parseTransform(style),
|
||||||
zIndex: parseZIndex(style.zIndex)
|
zIndex: parseZIndex(style.zIndex)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.image =
|
this.image =
|
||||||
node instanceof HTMLImageElement
|
// $FlowFixMe
|
||||||
? imageLoader.loadImage(node.currentSrc || node.src)
|
node.tagName === 'IMG' ? imageLoader.loadImage(node.currentSrc || node.src) : null;
|
||||||
: null;
|
|
||||||
this.bounds = parseBounds(node, this.isTransformed());
|
this.bounds = parseBounds(node, this.isTransformed());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
this.name = `${node.tagName.toLowerCase()}${node.id
|
this.name = `${node.tagName.toLowerCase()}${node.id
|
||||||
|
@ -36,17 +36,20 @@ const parseNodeTree = (
|
|||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader
|
||||||
): void => {
|
): void => {
|
||||||
node.childNodes.forEach((childNode: Node) => {
|
node.childNodes.forEach((childNode: Node) => {
|
||||||
if (childNode instanceof Text) {
|
if (childNode.nodeType === Node.TEXT_NODE) {
|
||||||
|
//$FlowFixMe
|
||||||
if (childNode.data.trim().length > 0) {
|
if (childNode.data.trim().length > 0) {
|
||||||
|
//$FlowFixMe
|
||||||
parent.textNodes.push(new TextContainer(childNode, parent));
|
parent.textNodes.push(new TextContainer(childNode, parent));
|
||||||
}
|
}
|
||||||
} else if (childNode instanceof HTMLElement) {
|
} else if (childNode.nodeType === Node.ELEMENT_NODE) {
|
||||||
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
||||||
const container = new NodeContainer(childNode, parent, imageLoader);
|
const childElement = flowRefineToHTMLElement(childNode);
|
||||||
|
const container = new NodeContainer(childElement, parent, imageLoader);
|
||||||
if (container.isVisible()) {
|
if (container.isVisible()) {
|
||||||
const treatAsRealStackingContext = createsRealStackingContext(
|
const treatAsRealStackingContext = createsRealStackingContext(
|
||||||
container,
|
container,
|
||||||
childNode
|
childElement
|
||||||
);
|
);
|
||||||
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
||||||
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
||||||
@ -61,10 +64,10 @@ const parseNodeTree = (
|
|||||||
treatAsRealStackingContext
|
treatAsRealStackingContext
|
||||||
);
|
);
|
||||||
parentStack.contexts.push(childStack);
|
parentStack.contexts.push(childStack);
|
||||||
parseNodeTree(childNode, container, childStack, imageLoader);
|
parseNodeTree(childElement, container, childStack, imageLoader);
|
||||||
} else {
|
} else {
|
||||||
stack.children.push(container);
|
stack.children.push(container);
|
||||||
parseNodeTree(childNode, container, stack, imageLoader);
|
parseNodeTree(childElement, container, stack, imageLoader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,3 +96,6 @@ const isBodyWithTransparentRoot = (container: NodeContainer, node: HTMLElement):
|
|||||||
container.parent.style.background.backgroundColor.isTransparent()
|
container.parent.style.background.backgroundColor.isTransparent()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//$FlowFixMe
|
||||||
|
const flowRefineToHTMLElement = (node: Node): HTMLElement => node;
|
||||||
|
76
src/index.js
76
src/index.js
@ -5,6 +5,8 @@ import {NodeParser} from './NodeParser';
|
|||||||
import CanvasRenderer from './CanvasRenderer';
|
import CanvasRenderer from './CanvasRenderer';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
import ImageLoader from './ImageLoader';
|
import ImageLoader from './ImageLoader';
|
||||||
|
import {Bounds} from './Bounds';
|
||||||
|
import {cloneWindow} from './Clone';
|
||||||
import Color from './Color';
|
import Color from './Color';
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
@ -12,36 +14,64 @@ export type Options = {
|
|||||||
imageTimeout: number,
|
imageTimeout: number,
|
||||||
proxy: string,
|
proxy: string,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
allowTaint: true
|
allowTaint: true,
|
||||||
|
type: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const html2canvas = (element: HTMLElement, options: Options): Promise<HTMLCanvasElement> => {
|
const html2canvas = (element: HTMLElement, options: Options): Promise<HTMLCanvasElement> => {
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
const imageLoader = new ImageLoader(options, logger);
|
|
||||||
const stack = NodeParser(element, imageLoader, logger);
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
|
|
||||||
const scale = window.devicePixelRatio;
|
const ownerDocument = element.ownerDocument;
|
||||||
const width = window.innerWidth;
|
const defaultView = ownerDocument.defaultView;
|
||||||
const height = window.innerHeight;
|
const windowBounds = new Bounds(
|
||||||
canvas.width = Math.floor(width * scale);
|
defaultView.pageXOffset,
|
||||||
canvas.height = Math.floor(height * scale);
|
defaultView.pageYOffset,
|
||||||
canvas.style.width = `${width}px`;
|
defaultView.innerWidth,
|
||||||
canvas.style.height = `${height}px`;
|
defaultView.innerHeight
|
||||||
|
);
|
||||||
|
|
||||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
return cloneWindow(
|
||||||
const backgroundColor =
|
ownerDocument,
|
||||||
element === element.ownerDocument.documentElement
|
ownerDocument,
|
||||||
? stack.container.style.background.backgroundColor.isTransparent()
|
windowBounds,
|
||||||
? element.ownerDocument.body instanceof HTMLElement
|
element,
|
||||||
? new Color(getComputedStyle(element.ownerDocument.body).backgroundColor)
|
options
|
||||||
: null
|
).then(([container, clonedElement]) => {
|
||||||
: stack.container.style.background.backgroundColor
|
if (__DEV__) {
|
||||||
: null;
|
logger.log(`Document cloned`);
|
||||||
|
}
|
||||||
|
|
||||||
return imageLoader.ready().then(imageStore => {
|
const imageLoader = new ImageLoader(options, logger);
|
||||||
const renderer = new CanvasRenderer(canvas, {scale, backgroundColor, imageStore});
|
const stack = NodeParser(clonedElement, imageLoader, logger);
|
||||||
return renderer.render(stack);
|
const canvas = ownerDocument.createElement('canvas');
|
||||||
|
const scale = defaultView.devicePixelRatio;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
canvas.width = Math.floor(width * scale);
|
||||||
|
canvas.height = Math.floor(height * scale);
|
||||||
|
canvas.style.width = `${width}px`;
|
||||||
|
canvas.style.height = `${height}px`;
|
||||||
|
|
||||||
|
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||||
|
const backgroundColor =
|
||||||
|
clonedElement === ownerDocument.documentElement
|
||||||
|
? stack.container.style.background.backgroundColor.isTransparent()
|
||||||
|
? ownerDocument.body instanceof HTMLElement
|
||||||
|
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
|
||||||
|
: null
|
||||||
|
: stack.container.style.background.backgroundColor
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return imageLoader.ready().then(imageStore => {
|
||||||
|
if (container.parentNode) {
|
||||||
|
container.parentNode.removeChild(container);
|
||||||
|
} else if (__DEV__) {
|
||||||
|
logger.log(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = new CanvasRenderer(canvas, {scale, backgroundColor, imageStore});
|
||||||
|
return renderer.render(stack);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user