Fix pseudoelement rendering (with nth-child selectors etc.)

This commit is contained in:
Niklas von Hertzen 2014-10-06 22:46:43 +03:00
parent d5430070a2
commit e103ad8219
8 changed files with 175 additions and 53 deletions

View File

@ -14,6 +14,6 @@
"globals": {
"jQuery": true
},
"predef": ["NodeParser", "NodeContainer", "StackingContext", "TextContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", "getBounds", "offsetBounds", "XHR",
"predef": ["NodeParser", "NodeContainer", "StackingContext", "TextContainer", "PseudoElementContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", "getBounds", "offsetBounds", "XHR",
"ImageContainer", "ProxyImageContainer", "DummyImageContainer", "Font", "FontMetrics", "GradientContainer", "LinearGradientContainer", "WebkitGradientContainer", "SVGContainer", "SVGNodeContainer", "FrameContainer", "html2canvas", "log", "smallImage", "parseBackgrounds", "loadUrlDocument", "decode64", "Proxy", "ProxyURL"]
}

109
dist/html2canvas.js vendored
View File

@ -5,7 +5,7 @@
Released under MIT License
*/
(function(window, document, module, exports, undefined){
(function(window, document, module, exports, global, define, undefined){
/*
Copyright (c) 2013 Yehuda Katz, Tom Dale, and contributors
@ -1164,8 +1164,26 @@ function NodeContainer(node, parent) {
this.backgroundImages = null;
this.transformData = null;
this.transformMatrix = null;
this.isPseudoElement = false;
this.opacity = null;
}
NodeContainer.prototype.cloneTo = function(stack) {
stack.visible = this.visible;
stack.borders = this.borders;
stack.bounds = this.bounds;
stack.clip = this.clip;
stack.backgroundClip = this.backgroundClip;
stack.computedStyles = this.computedStyles;
stack.styles = this.styles;
stack.backgroundImages = this.backgroundImages;
stack.opacity = this.opacity;
};
NodeContainer.prototype.getOpacity = function() {
return this.opacity === null ? (this.opacity = this.cssFloat('opacity')) : this.opacity;
};
NodeContainer.prototype.assignStack = function(stack) {
this.stack = stack;
stack.children.push(this);
@ -1182,7 +1200,7 @@ NodeContainer.prototype.isElementVisible = function() {
NodeContainer.prototype.css = function(attribute) {
if (!this.computedStyles) {
this.computedStyles = this.computedStyle(null);
this.computedStyles = this.isPseudoElement ? this.parent.computedStyle(this.before ? ":before" : ":after") : this.computedStyle(null);
}
return this.styles[attribute] || (this.styles[attribute] = this.computedStyles[attribute]);
@ -1538,11 +1556,12 @@ function NodeParser(element, renderer, support, imageLoader, options) {
}).map(this.getPseudoElements, this));
this.fontMetrics = new FontMetrics();
log("Fetched nodes, total:", this.nodes.length);
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Start fetching images");
this.images = imageLoader.fetch(this.nodes.filter(isElement));
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Creating stacking contexts");
this.createStackingContexts();
log("Sorting stacking contexts");
@ -1566,14 +1585,22 @@ function NodeParser(element, renderer, support, imageLoader, options) {
NodeParser.prototype.calculateOverflowClips = function() {
this.nodes.forEach(function(container) {
if (isElement(container)) {
if (isPseudoElement(container)) {
container.appendToDOM();
}
container.borders = this.parseBorders(container);
var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
if (isPseudoElement(container)) {
container.cleanDOM();
}
} else if (isTextNode(container)) {
container.clip = hasParentClip(container) ? container.parent.clip : [];
}
container.bounds = null;
if (!isPseudoElement(container)) {
container.bounds = null;
}
}, this);
};
@ -1597,8 +1624,8 @@ NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
NodeParser.prototype.createPseudoHideStyles = function(document) {
var hidePseudoElements = document.createElement('style');
hidePseudoElements.innerHTML = '.' + this.pseudoHideClass + ':before { content: "" !important; display: none !important; }' +
'.' + this.pseudoHideClass + ':after { content: "" !important; display: none !important; }';
hidePseudoElements.innerHTML = '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + ':before { content: "" !important; display: none !important; }' +
'.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER + ':after { content: "" !important; display: none !important; }';
document.body.appendChild(hidePseudoElements);
};
@ -1609,18 +1636,12 @@ NodeParser.prototype.getPseudoElements = function(container) {
var after = this.getPseudoElement(container, ":after");
if (before) {
container.node.insertBefore(before[0].node, container.node.firstChild);
nodes.push(before);
}
if (after) {
container.node.appendChild(after[0].node);
nodes.push(after);
}
if (before || after) {
container.node.className += " " + this.pseudoHideClass;
}
}
return flatten(nodes);
};
@ -1640,14 +1661,14 @@ NodeParser.prototype.getPseudoElement = function(container, type) {
var content = stripQuotes(style.content);
var isImage = content.substr(0, 3) === 'url';
var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement');
var pseudoContainer = new NodeContainer(pseudoNode, container);
var pseudoContainer = new PseudoElementContainer(pseudoNode, container, type);
for (var i = style.length-1; i >= 0; i--) {
var property = toCamelCase(style.item(i));
pseudoNode.style[property] = style[property];
}
pseudoNode.className = this.pseudoHideClass;
pseudoNode.className = PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
if (isImage) {
pseudoNode.src = parseBackgrounds(content)[0].args[0];
@ -1668,12 +1689,8 @@ NodeParser.prototype.getChildren = function(parentContainer) {
};
NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent);
stack.visible = container.visible;
stack.borders = container.borders;
stack.bounds = container.bounds;
stack.clip = container.clip;
stack.backgroundClip = container.backgroundClip;
var stack = new StackingContext(hasOwnStacking, container.getOpacity(), container.node, container.parent);
container.cloneTo(stack);
var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
parentStack.contexts.push(stack);
container.stack = stack;
@ -1770,7 +1787,13 @@ NodeParser.prototype.paint = function(container) {
if (container instanceof ClearTransform) {
this.renderer.ctx.restore();
} else if (isTextNode(container)) {
if (isPseudoElement(container.parent)) {
container.parent.appendToDOM();
}
this.paintText(container);
if (isPseudoElement(container.parent)) {
container.parent.cleanDOM();
}
} else {
this.paintNode(container);
}
@ -1787,6 +1810,7 @@ NodeParser.prototype.paintNode = function(container) {
this.renderer.setTransform(container.parseTransform());
}
}
var bounds = container.parseBounds();
this.renderer.clip(container.backgroundClip, function() {
this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
@ -2000,8 +2024,6 @@ NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, bor
return borderArgs;
};
NodeParser.prototype.pseudoHideClass = "___html2canvas___pseudoelement";
function getCurvePoints(x, y, r1, r2) {
var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
var ox = (r1) * kappa, // control point offset horizontal
@ -2195,6 +2217,10 @@ function isElement(container) {
return container.node.nodeType === Node.ELEMENT_NODE;
}
function isPseudoElement(container) {
return container.isPseudoElement === true;
}
function isTextNode(container) {
return container.node.nodeType === Node.TEXT_NODE;
}
@ -2204,7 +2230,7 @@ function zIndexSort(a, b) {
}
function hasOpacity(container) {
return container.css("opacity") < 1;
return container.getOpacity() < 1;
}
function bind(callback, context) {
@ -2340,6 +2366,41 @@ function ProxyImageContainer(src, proxy) {
});
}
function PseudoElementContainer(node, parent, type) {
NodeContainer.call(this, node, parent);
this.isPseudoElement = true;
this.before = type === ":before";
}
PseudoElementContainer.prototype.cloneTo = function(stack) {
PseudoElementContainer.prototype.cloneTo.call(this, stack);
stack.isPseudoElement = true;
stack.before = this.before;
};
PseudoElementContainer.prototype = Object.create(NodeContainer.prototype);
PseudoElementContainer.prototype.appendToDOM = function() {
if (this.before) {
this.parent.node.insertBefore(this.node, this.parent.node.firstChild);
} else {
this.parent.node.appendChild(this.node);
}
this.parent.node.className += " " + this.getHideClass();
};
PseudoElementContainer.prototype.cleanDOM = function() {
this.node.parentNode.removeChild(this.node);
this.parent.node.className = this.parent.node.className.replace(this.getHideClass(), "");
};
PseudoElementContainer.prototype.getHideClass = function() {
return this["PSEUDO_HIDE_ELEMENT_CLASS_" + (this.before ? "BEFORE" : "AFTER")];
};
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = "___html2canvas___pseudoelement_before";
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER = "___html2canvas___pseudoelement_after";
function Renderer(width, height, images, options, document) {
this.width = width;
this.height = height;

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,26 @@ function NodeContainer(node, parent) {
this.backgroundImages = null;
this.transformData = null;
this.transformMatrix = null;
this.isPseudoElement = false;
this.opacity = null;
}
NodeContainer.prototype.cloneTo = function(stack) {
stack.visible = this.visible;
stack.borders = this.borders;
stack.bounds = this.bounds;
stack.clip = this.clip;
stack.backgroundClip = this.backgroundClip;
stack.computedStyles = this.computedStyles;
stack.styles = this.styles;
stack.backgroundImages = this.backgroundImages;
stack.opacity = this.opacity;
};
NodeContainer.prototype.getOpacity = function() {
return this.opacity === null ? (this.opacity = this.cssFloat('opacity')) : this.opacity;
};
NodeContainer.prototype.assignStack = function(stack) {
this.stack = stack;
stack.children.push(this);
@ -31,7 +49,7 @@ NodeContainer.prototype.isElementVisible = function() {
NodeContainer.prototype.css = function(attribute) {
if (!this.computedStyles) {
this.computedStyles = this.computedStyle(null);
this.computedStyles = this.isPseudoElement ? this.parent.computedStyle(this.before ? ":before" : ":after") : this.computedStyle(null);
}
return this.styles[attribute] || (this.styles[attribute] = this.computedStyles[attribute]);

View File

@ -19,11 +19,12 @@ function NodeParser(element, renderer, support, imageLoader, options) {
}).map(this.getPseudoElements, this));
this.fontMetrics = new FontMetrics();
log("Fetched nodes, total:", this.nodes.length);
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Start fetching images");
this.images = imageLoader.fetch(this.nodes.filter(isElement));
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Creating stacking contexts");
this.createStackingContexts();
log("Sorting stacking contexts");
@ -47,14 +48,22 @@ function NodeParser(element, renderer, support, imageLoader, options) {
NodeParser.prototype.calculateOverflowClips = function() {
this.nodes.forEach(function(container) {
if (isElement(container)) {
if (isPseudoElement(container)) {
container.appendToDOM();
}
container.borders = this.parseBorders(container);
var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
if (isPseudoElement(container)) {
container.cleanDOM();
}
} else if (isTextNode(container)) {
container.clip = hasParentClip(container) ? container.parent.clip : [];
}
container.bounds = null;
if (!isPseudoElement(container)) {
container.bounds = null;
}
}, this);
};
@ -78,8 +87,8 @@ NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
NodeParser.prototype.createPseudoHideStyles = function(document) {
var hidePseudoElements = document.createElement('style');
hidePseudoElements.innerHTML = '.' + this.pseudoHideClass + ':before { content: "" !important; display: none !important; }' +
'.' + this.pseudoHideClass + ':after { content: "" !important; display: none !important; }';
hidePseudoElements.innerHTML = '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + ':before { content: "" !important; display: none !important; }' +
'.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER + ':after { content: "" !important; display: none !important; }';
document.body.appendChild(hidePseudoElements);
};
@ -90,18 +99,12 @@ NodeParser.prototype.getPseudoElements = function(container) {
var after = this.getPseudoElement(container, ":after");
if (before) {
container.node.insertBefore(before[0].node, container.node.firstChild);
nodes.push(before);
}
if (after) {
container.node.appendChild(after[0].node);
nodes.push(after);
}
if (before || after) {
container.node.className += " " + this.pseudoHideClass;
}
}
return flatten(nodes);
};
@ -121,14 +124,14 @@ NodeParser.prototype.getPseudoElement = function(container, type) {
var content = stripQuotes(style.content);
var isImage = content.substr(0, 3) === 'url';
var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement');
var pseudoContainer = new NodeContainer(pseudoNode, container);
var pseudoContainer = new PseudoElementContainer(pseudoNode, container, type);
for (var i = style.length-1; i >= 0; i--) {
var property = toCamelCase(style.item(i));
pseudoNode.style[property] = style[property];
}
pseudoNode.className = this.pseudoHideClass;
pseudoNode.className = PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
if (isImage) {
pseudoNode.src = parseBackgrounds(content)[0].args[0];
@ -149,12 +152,8 @@ NodeParser.prototype.getChildren = function(parentContainer) {
};
NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent);
stack.visible = container.visible;
stack.borders = container.borders;
stack.bounds = container.bounds;
stack.clip = container.clip;
stack.backgroundClip = container.backgroundClip;
var stack = new StackingContext(hasOwnStacking, container.getOpacity(), container.node, container.parent);
container.cloneTo(stack);
var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
parentStack.contexts.push(stack);
container.stack = stack;
@ -251,7 +250,13 @@ NodeParser.prototype.paint = function(container) {
if (container instanceof ClearTransform) {
this.renderer.ctx.restore();
} else if (isTextNode(container)) {
if (isPseudoElement(container.parent)) {
container.parent.appendToDOM();
}
this.paintText(container);
if (isPseudoElement(container.parent)) {
container.parent.cleanDOM();
}
} else {
this.paintNode(container);
}
@ -268,6 +273,7 @@ NodeParser.prototype.paintNode = function(container) {
this.renderer.setTransform(container.parseTransform());
}
}
var bounds = container.parseBounds();
this.renderer.clip(container.backgroundClip, function() {
this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
@ -481,8 +487,6 @@ NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, bor
return borderArgs;
};
NodeParser.prototype.pseudoHideClass = "___html2canvas___pseudoelement";
function getCurvePoints(x, y, r1, r2) {
var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
var ox = (r1) * kappa, // control point offset horizontal
@ -676,6 +680,10 @@ function isElement(container) {
return container.node.nodeType === Node.ELEMENT_NODE;
}
function isPseudoElement(container) {
return container.isPseudoElement === true;
}
function isTextNode(container) {
return container.node.nodeType === Node.TEXT_NODE;
}
@ -685,7 +693,7 @@ function zIndexSort(a, b) {
}
function hasOpacity(container) {
return container.css("opacity") < 1;
return container.getOpacity() < 1;
}
function bind(callback, context) {

View File

@ -0,0 +1,34 @@
function PseudoElementContainer(node, parent, type) {
NodeContainer.call(this, node, parent);
this.isPseudoElement = true;
this.before = type === ":before";
}
PseudoElementContainer.prototype.cloneTo = function(stack) {
PseudoElementContainer.prototype.cloneTo.call(this, stack);
stack.isPseudoElement = true;
stack.before = this.before;
};
PseudoElementContainer.prototype = Object.create(NodeContainer.prototype);
PseudoElementContainer.prototype.appendToDOM = function() {
if (this.before) {
this.parent.node.insertBefore(this.node, this.parent.node.firstChild);
} else {
this.parent.node.appendChild(this.node);
}
this.parent.node.className += " " + this.getHideClass();
};
PseudoElementContainer.prototype.cleanDOM = function() {
this.node.parentNode.removeChild(this.node);
this.parent.node.className = this.parent.node.className.replace(this.getHideClass(), "");
};
PseudoElementContainer.prototype.getHideClass = function() {
return this["PSEUDO_HIDE_ELEMENT_CLASS_" + (this.before ? "BEFORE" : "AFTER")];
};
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = "___html2canvas___pseudoelement_before";
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER = "___html2canvas___pseudoelement_after";

View File

@ -6,9 +6,9 @@
<script type="text/javascript" src="../test.js"></script>
<style>
:root .text *::before {
content:" root before!";
content:" root before!";
}
.text *::before {
content:" before!";
}
@ -27,6 +27,7 @@
.background *::before{
background: url(../assets/image_1.jpg);
border: 5px solid red;
}
.background *::after{

View File

@ -11,7 +11,7 @@ var h2cSelector, h2cOptions;
document.write('<script type="text/javascript" src="' + src + '.js?' + Math.random() + '"></script>');
}
var sources = ['log', 'punycode/punycode', 'core', 'nodecontainer', 'stackingcontext', 'textcontainer', 'support', 'imagecontainer', 'dummyimagecontainer', 'proxyimagecontainer', 'gradientcontainer',
var sources = ['log', 'punycode/punycode', 'core', 'nodecontainer', 'pseudoelementcontainer', 'stackingcontext', 'textcontainer', 'support', 'imagecontainer', 'dummyimagecontainer', 'proxyimagecontainer', 'gradientcontainer',
'lineargradientcontainer', 'webkitgradientcontainer', 'svgcontainer', 'svgnodecontainer', 'imageloader', 'nodeparser', 'font', 'fontmetrics', 'renderer', 'promise', 'xhr', 'framecontainer', 'proxy', 'renderers/canvas'];
['/tests/assets/jquery-1.6.2'].concat(window.location.search === "?selenium" ? ['/dist/html2canvas'] : sources.map(function(src) { return '/src/' + src; })).forEach(appendScript);