2017-07-29 05:07:42 +03:00
|
|
|
/* @flow */
|
|
|
|
'use strict';
|
|
|
|
import type ImageLoader from './ImageLoader';
|
|
|
|
import type Logger from './Logger';
|
|
|
|
import StackingContext from './StackingContext';
|
|
|
|
import NodeContainer from './NodeContainer';
|
|
|
|
import TextContainer from './TextContainer';
|
2017-08-04 19:00:17 +03:00
|
|
|
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
|
2017-08-06 12:37:10 +03:00
|
|
|
import {copyCSSStyles} from './Util';
|
2017-07-29 05:07:42 +03:00
|
|
|
|
|
|
|
export const NodeParser = (
|
|
|
|
node: HTMLElement,
|
|
|
|
imageLoader: ImageLoader,
|
|
|
|
logger: Logger
|
|
|
|
): StackingContext => {
|
|
|
|
if (__DEV__) {
|
|
|
|
logger.log(`Starting node parsing`);
|
|
|
|
}
|
|
|
|
|
2017-08-09 06:52:42 +03:00
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
const container = new NodeContainer(node, null, imageLoader, index++);
|
2017-08-06 12:37:10 +03:00
|
|
|
const stack = new StackingContext(container, null, true);
|
|
|
|
|
|
|
|
createPseudoHideStyles(node.ownerDocument);
|
2017-08-09 06:52:42 +03:00
|
|
|
parseNodeTree(node, container, stack, imageLoader, index);
|
2017-07-29 05:07:42 +03:00
|
|
|
|
|
|
|
if (__DEV__) {
|
|
|
|
logger.log(`Finished parsing node tree`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return stack;
|
|
|
|
};
|
|
|
|
|
|
|
|
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
|
2017-08-03 18:46:29 +03:00
|
|
|
const URL_REGEXP = /^url\((.+)\)$/i;
|
|
|
|
const PSEUDO_BEFORE = ':before';
|
|
|
|
const PSEUDO_AFTER = ':after';
|
|
|
|
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
|
|
|
|
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
|
2017-07-29 05:07:42 +03:00
|
|
|
|
|
|
|
const parseNodeTree = (
|
|
|
|
node: HTMLElement,
|
|
|
|
parent: NodeContainer,
|
|
|
|
stack: StackingContext,
|
2017-08-09 06:52:42 +03:00
|
|
|
imageLoader: ImageLoader,
|
|
|
|
index: number
|
2017-07-29 05:07:42 +03:00
|
|
|
): void => {
|
2017-08-01 18:27:12 +03:00
|
|
|
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
|
|
|
nextNode = childNode.nextSibling;
|
2017-08-04 14:27:35 +03:00
|
|
|
const defaultView = childNode.ownerDocument.defaultView;
|
2017-08-04 19:00:17 +03:00
|
|
|
if (childNode instanceof defaultView.Text || childNode instanceof Text) {
|
2017-07-29 05:07:42 +03:00
|
|
|
if (childNode.data.trim().length > 0) {
|
2017-08-04 19:00:17 +03:00
|
|
|
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
|
2017-07-29 05:07:42 +03:00
|
|
|
}
|
2017-08-04 19:00:17 +03:00
|
|
|
} else if (
|
|
|
|
childNode instanceof defaultView.HTMLElement ||
|
|
|
|
childNode instanceof HTMLElement
|
|
|
|
) {
|
2017-07-29 05:07:42 +03:00
|
|
|
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
2017-08-04 14:27:35 +03:00
|
|
|
inlinePseudoElement(childNode, PSEUDO_BEFORE);
|
|
|
|
inlinePseudoElement(childNode, PSEUDO_AFTER);
|
2017-08-09 06:52:42 +03:00
|
|
|
const container = new NodeContainer(childNode, parent, imageLoader, index++);
|
2017-07-29 05:07:42 +03:00
|
|
|
if (container.isVisible()) {
|
2017-08-04 19:00:17 +03:00
|
|
|
if (childNode.tagName === 'INPUT') {
|
|
|
|
// $FlowFixMe
|
|
|
|
inlineInputElement(childNode, container);
|
|
|
|
} else if (childNode.tagName === 'TEXTAREA') {
|
|
|
|
// $FlowFixMe
|
|
|
|
inlineTextAreaElement(childNode, container);
|
|
|
|
} else if (childNode.tagName === 'SELECT') {
|
|
|
|
// $FlowFixMe
|
|
|
|
inlineSelectElement(childNode, container);
|
|
|
|
}
|
2017-08-05 16:40:22 +03:00
|
|
|
|
|
|
|
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
|
2017-07-29 05:07:42 +03:00
|
|
|
const treatAsRealStackingContext = createsRealStackingContext(
|
|
|
|
container,
|
2017-08-04 14:27:35 +03:00
|
|
|
childNode
|
2017-07-29 05:07:42 +03:00
|
|
|
);
|
|
|
|
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
|
|
|
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
|
|
|
// which actually create a new stacking context should be considered part of the parent stacking context
|
|
|
|
const parentStack =
|
|
|
|
treatAsRealStackingContext || container.isPositioned()
|
|
|
|
? stack.getRealParentStackingContext()
|
|
|
|
: stack;
|
|
|
|
const childStack = new StackingContext(
|
|
|
|
container,
|
|
|
|
parentStack,
|
|
|
|
treatAsRealStackingContext
|
|
|
|
);
|
|
|
|
parentStack.contexts.push(childStack);
|
2017-08-05 16:40:22 +03:00
|
|
|
if (SHOULD_TRAVERSE_CHILDREN) {
|
2017-08-09 06:52:42 +03:00
|
|
|
parseNodeTree(childNode, container, childStack, imageLoader, index);
|
2017-08-05 16:40:22 +03:00
|
|
|
}
|
2017-07-29 05:07:42 +03:00
|
|
|
} else {
|
|
|
|
stack.children.push(container);
|
2017-08-05 16:40:22 +03:00
|
|
|
if (SHOULD_TRAVERSE_CHILDREN) {
|
2017-08-09 06:52:42 +03:00
|
|
|
parseNodeTree(childNode, container, stack, imageLoader, index);
|
2017-08-05 16:40:22 +03:00
|
|
|
}
|
2017-07-29 05:07:42 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-08-13 12:48:37 +03:00
|
|
|
} else if (
|
|
|
|
childNode instanceof defaultView.SVGSVGElement ||
|
|
|
|
childNode instanceof SVGSVGElement
|
|
|
|
) {
|
|
|
|
const container = new NodeContainer(childNode, parent, imageLoader, index++);
|
|
|
|
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
|
|
|
|
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
|
|
|
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
|
|
|
// which actually create a new stacking context should be considered part of the parent stacking context
|
|
|
|
const parentStack =
|
|
|
|
treatAsRealStackingContext || container.isPositioned()
|
|
|
|
? stack.getRealParentStackingContext()
|
|
|
|
: stack;
|
|
|
|
const childStack = new StackingContext(
|
|
|
|
container,
|
|
|
|
parentStack,
|
|
|
|
treatAsRealStackingContext
|
|
|
|
);
|
|
|
|
parentStack.contexts.push(childStack);
|
|
|
|
} else {
|
|
|
|
stack.children.push(container);
|
|
|
|
}
|
2017-07-29 05:07:42 +03:00
|
|
|
}
|
2017-08-01 18:27:12 +03:00
|
|
|
}
|
2017-07-29 05:07:42 +03:00
|
|
|
};
|
|
|
|
|
2017-08-03 18:46:29 +03:00
|
|
|
const inlinePseudoElement = (node: HTMLElement, pseudoElt: ':before' | ':after'): void => {
|
|
|
|
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
|
|
|
|
if (
|
|
|
|
!style ||
|
|
|
|
!style.content ||
|
|
|
|
style.content === 'none' ||
|
|
|
|
style.content === '-moz-alt-content' ||
|
|
|
|
style.display === 'none'
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const content = stripQuotes(style.content);
|
|
|
|
const image = content.match(URL_REGEXP);
|
|
|
|
const anonymousReplacedElement = node.ownerDocument.createElement(
|
|
|
|
image ? 'img' : 'html2canvaspseudoelement'
|
|
|
|
);
|
|
|
|
if (image) {
|
|
|
|
// $FlowFixMe
|
2017-08-03 18:54:44 +03:00
|
|
|
anonymousReplacedElement.src = stripQuotes(image[1]);
|
2017-08-03 18:46:29 +03:00
|
|
|
} else {
|
2017-08-04 19:00:17 +03:00
|
|
|
anonymousReplacedElement.textContent = content;
|
2017-08-03 18:46:29 +03:00
|
|
|
}
|
|
|
|
|
2017-08-06 12:37:10 +03:00
|
|
|
copyCSSStyles(style, anonymousReplacedElement);
|
2017-08-05 16:59:48 +03:00
|
|
|
|
2017-08-03 18:46:29 +03:00
|
|
|
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
|
|
|
node.className +=
|
|
|
|
pseudoElt === PSEUDO_BEFORE
|
|
|
|
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
|
|
|
|
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
|
|
|
if (pseudoElt === PSEUDO_BEFORE) {
|
|
|
|
node.insertBefore(anonymousReplacedElement, node.firstChild);
|
|
|
|
} else {
|
|
|
|
node.appendChild(anonymousReplacedElement);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-13 12:48:37 +03:00
|
|
|
const createsRealStackingContext = (
|
|
|
|
container: NodeContainer,
|
|
|
|
node: HTMLElement | SVGSVGElement
|
|
|
|
): boolean => {
|
2017-07-29 05:07:42 +03:00
|
|
|
return (
|
|
|
|
container.isRootElement() ||
|
|
|
|
container.isPositionedWithZIndex() ||
|
|
|
|
container.style.opacity < 1 ||
|
|
|
|
container.isTransformed() ||
|
|
|
|
isBodyWithTransparentRoot(container, node)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const createsStackingContext = (container: NodeContainer): boolean => {
|
|
|
|
return container.isPositioned() || container.isFloating();
|
|
|
|
};
|
|
|
|
|
2017-08-13 12:48:37 +03:00
|
|
|
const isBodyWithTransparentRoot = (
|
|
|
|
container: NodeContainer,
|
|
|
|
node: HTMLElement | SVGSVGElement
|
|
|
|
): boolean => {
|
2017-07-29 05:07:42 +03:00
|
|
|
return (
|
|
|
|
node.nodeName === 'BODY' &&
|
|
|
|
container.parent instanceof NodeContainer &&
|
|
|
|
container.parent.style.background.backgroundColor.isTransparent()
|
|
|
|
);
|
|
|
|
};
|
2017-08-01 15:54:18 +03:00
|
|
|
|
2017-08-03 18:46:29 +03:00
|
|
|
const stripQuotes = (content: string): string => {
|
|
|
|
const first = content.substr(0, 1);
|
|
|
|
return first === content.substr(content.length - 1) && first.match(/['"]/)
|
|
|
|
? content.substr(1, content.length - 2)
|
|
|
|
: content;
|
|
|
|
};
|
|
|
|
|
|
|
|
const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
|
|
|
content: "" !important;
|
|
|
|
display: none !important;
|
|
|
|
}`;
|
|
|
|
|
|
|
|
const createPseudoHideStyles = (document: Document) => {
|
|
|
|
createStyles(
|
|
|
|
document,
|
|
|
|
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
|
|
|
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const createStyles = (document: Document, styles) => {
|
|
|
|
const style = document.createElement('style');
|
|
|
|
style.innerHTML = styles;
|
|
|
|
if (document.body) {
|
|
|
|
document.body.appendChild(style);
|
|
|
|
}
|
|
|
|
};
|