Skip to content

Commit 585a96a

Browse files
committed
Preserve scrolling positions of nodes on clone (niklasvh#511)
1 parent 9a0d43d commit 585a96a

5 files changed

Lines changed: 107 additions & 124 deletions

File tree

dist/html2canvas.js

Lines changed: 34 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,85 +1542,63 @@ process.umask = function() { return 0; };
15421542
var log = require('./log');
15431543
var Promise = require('./promise');
15441544

1545-
var html2canvasCanvasCloneAttribute = "data-html2canvas-canvas-clone";
1546-
var html2canvasCanvasCloneIndex = 0;
1547-
1548-
function cloneNodeValues(document, clone, nodeName) {
1549-
var originalNodes = document.getElementsByTagName(nodeName);
1550-
var clonedNodes = clone.getElementsByTagName(nodeName);
1551-
var count = originalNodes.length;
1552-
for (var i = 0; i < count; i++) {
1553-
clonedNodes[i].value = originalNodes[i].value;
1554-
}
1555-
}
1556-
15571545
function restoreOwnerScroll(ownerDocument, x, y) {
15581546
if (ownerDocument.defaultView && (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) {
15591547
ownerDocument.defaultView.scrollTo(x, y);
15601548
}
15611549
}
15621550

1563-
function labelCanvasElements(ownerDocument) {
1564-
[].slice.call(ownerDocument.querySelectorAll("canvas"), 0).forEach(function(canvas) {
1565-
canvas.setAttribute(html2canvasCanvasCloneAttribute, "canvas-" + html2canvasCanvasCloneIndex++);
1566-
});
1567-
}
1568-
1569-
function cloneCanvasContents(ownerDocument, documentClone) {
1570-
[].slice.call(ownerDocument.querySelectorAll("[" + html2canvasCanvasCloneAttribute + "]"), 0).forEach(function(canvas) {
1571-
try {
1572-
var clonedCanvas = documentClone.querySelector('[' + html2canvasCanvasCloneAttribute + '="' + canvas.getAttribute(html2canvasCanvasCloneAttribute) + '"]');
1573-
if (clonedCanvas) {
1574-
clonedCanvas.width = canvas.width;
1575-
clonedCanvas.height = canvas.height;
1576-
clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0);
1577-
}
1578-
} catch(e) {
1579-
log("Unable to copy canvas content from", canvas, e);
1580-
}
1581-
canvas.removeAttribute(html2canvasCanvasCloneAttribute);
1582-
});
1583-
}
1584-
1585-
function removeScriptNodes(parent) {
1586-
[].slice.call(parent.childNodes, 0).filter(isElementNode).forEach(function(node) {
1587-
if (node.tagName === "SCRIPT") {
1588-
parent.removeChild(node);
1589-
} else {
1590-
removeScriptNodes(node);
1551+
function cloneCanvasContents(canvas, clonedCanvas) {
1552+
try {
1553+
if (clonedCanvas) {
1554+
clonedCanvas.width = canvas.width;
1555+
clonedCanvas.height = canvas.height;
1556+
clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0);
15911557
}
1592-
});
1593-
return parent;
1594-
}
1595-
1596-
function isIE9() {
1597-
return document.documentMode && document.documentMode <= 9;
1558+
} catch(e) {
1559+
log("Unable to copy canvas content from", canvas, e);
1560+
}
15981561
}
15991562

1600-
// https://github.com/niklasvh/html2canvas/issues/503
1601-
function cloneNodeIE9(node, javascriptEnabled) {
1563+
function cloneNode(node, javascriptEnabled) {
16021564
var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false);
16031565

16041566
var child = node.firstChild;
16051567
while(child) {
16061568
if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') {
1607-
clone.appendChild(cloneNodeIE9(child, javascriptEnabled));
1569+
clone.appendChild(cloneNode(child, javascriptEnabled));
16081570
}
16091571
child = child.nextSibling;
16101572
}
16111573

1574+
if (node.nodeType === 1) {
1575+
clone._scrollTop = node.scrollTop;
1576+
clone._scrollLeft = node.scrollLeft;
1577+
if (node.nodeName === "CANVAS") {
1578+
cloneCanvasContents(node, clone);
1579+
} else if (node.nodeName === "TEXTAREA" || node.nodeName === "SELECT") {
1580+
clone.value = node.value;
1581+
}
1582+
}
1583+
16121584
return clone;
16131585
}
16141586

1587+
function initNode(node) {
1588+
if (node.nodeType === 1) {
1589+
node.scrollTop = node._scrollTop;
1590+
node.scrollLeft = node._scrollLeft;
16151591

1616-
1617-
function isElementNode(node) {
1618-
return node.nodeType === Node.ELEMENT_NODE;
1592+
var child = node.firstChild;
1593+
while(child) {
1594+
initNode(child);
1595+
child = child.nextSibling;
1596+
}
1597+
}
16191598
}
16201599

16211600
module.exports = function(ownerDocument, containerDocument, width, height, options, x ,y) {
1622-
labelCanvasElements(ownerDocument);
1623-
var documentElement = isIE9() ? cloneNodeIE9(ownerDocument.documentElement, options.javascriptEnabled) : ownerDocument.documentElement.cloneNode(true);
1601+
var documentElement = cloneNode(ownerDocument.documentElement, options.javascriptEnabled);
16241602
var container = containerDocument.createElement("iframe");
16251603

16261604
container.className = "html2canvas-container";
@@ -1637,16 +1615,13 @@ module.exports = function(ownerDocument, containerDocument, width, height, optio
16371615
return new Promise(function(resolve) {
16381616
var documentClone = container.contentWindow.document;
16391617

1640-
cloneNodeValues(ownerDocument.documentElement, documentElement, "textarea");
1641-
cloneNodeValues(ownerDocument.documentElement, documentElement, "select");
1642-
16431618
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
16441619
if window url is about:blank, we can assign the url to current by writing onto the document
16451620
*/
16461621
container.contentWindow.onload = container.onload = function() {
16471622
var interval = setInterval(function() {
16481623
if (documentClone.body.childNodes.length > 0) {
1649-
cloneCanvasContents(ownerDocument, documentClone);
1624+
initNode(documentClone.documentElement);
16501625
clearInterval(interval);
16511626
if (options.type === "view") {
16521627
container.contentWindow.scrollTo(x, y);
@@ -1660,7 +1635,7 @@ module.exports = function(ownerDocument, containerDocument, width, height, optio
16601635
documentClone.write("<!DOCTYPE html><html></html>");
16611636
// Chrome scrolls the parent document for some reason after the write to the cloned window???
16621637
restoreOwnerScroll(ownerDocument, x, y);
1663-
documentClone.replaceChild(options.javascriptEnabled === true ? documentClone.adoptNode(documentElement) : removeScriptNodes(documentClone.adoptNode(documentElement)), documentClone.documentElement);
1638+
documentClone.replaceChild(documentClone.adoptNode(documentElement), documentClone.documentElement);
16641639
documentClone.close();
16651640
});
16661641
};

dist/html2canvas.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/clone.js

Lines changed: 34 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,63 @@
11
var log = require('./log');
22
var Promise = require('./promise');
33

4-
var html2canvasCanvasCloneAttribute = "data-html2canvas-canvas-clone";
5-
var html2canvasCanvasCloneIndex = 0;
6-
7-
function cloneNodeValues(document, clone, nodeName) {
8-
var originalNodes = document.getElementsByTagName(nodeName);
9-
var clonedNodes = clone.getElementsByTagName(nodeName);
10-
var count = originalNodes.length;
11-
for (var i = 0; i < count; i++) {
12-
clonedNodes[i].value = originalNodes[i].value;
13-
}
14-
}
15-
164
function restoreOwnerScroll(ownerDocument, x, y) {
175
if (ownerDocument.defaultView && (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) {
186
ownerDocument.defaultView.scrollTo(x, y);
197
}
208
}
219

22-
function labelCanvasElements(ownerDocument) {
23-
[].slice.call(ownerDocument.querySelectorAll("canvas"), 0).forEach(function(canvas) {
24-
canvas.setAttribute(html2canvasCanvasCloneAttribute, "canvas-" + html2canvasCanvasCloneIndex++);
25-
});
26-
}
27-
28-
function cloneCanvasContents(ownerDocument, documentClone) {
29-
[].slice.call(ownerDocument.querySelectorAll("[" + html2canvasCanvasCloneAttribute + "]"), 0).forEach(function(canvas) {
30-
try {
31-
var clonedCanvas = documentClone.querySelector('[' + html2canvasCanvasCloneAttribute + '="' + canvas.getAttribute(html2canvasCanvasCloneAttribute) + '"]');
32-
if (clonedCanvas) {
33-
clonedCanvas.width = canvas.width;
34-
clonedCanvas.height = canvas.height;
35-
clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0);
36-
}
37-
} catch(e) {
38-
log("Unable to copy canvas content from", canvas, e);
39-
}
40-
canvas.removeAttribute(html2canvasCanvasCloneAttribute);
41-
});
42-
}
43-
44-
function removeScriptNodes(parent) {
45-
[].slice.call(parent.childNodes, 0).filter(isElementNode).forEach(function(node) {
46-
if (node.tagName === "SCRIPT") {
47-
parent.removeChild(node);
48-
} else {
49-
removeScriptNodes(node);
10+
function cloneCanvasContents(canvas, clonedCanvas) {
11+
try {
12+
if (clonedCanvas) {
13+
clonedCanvas.width = canvas.width;
14+
clonedCanvas.height = canvas.height;
15+
clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0);
5016
}
51-
});
52-
return parent;
53-
}
54-
55-
function isIE9() {
56-
return document.documentMode && document.documentMode <= 9;
17+
} catch(e) {
18+
log("Unable to copy canvas content from", canvas, e);
19+
}
5720
}
5821

59-
// https://github.com/niklasvh/html2canvas/issues/503
60-
function cloneNodeIE9(node, javascriptEnabled) {
22+
function cloneNode(node, javascriptEnabled) {
6123
var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false);
6224

6325
var child = node.firstChild;
6426
while(child) {
6527
if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') {
66-
clone.appendChild(cloneNodeIE9(child, javascriptEnabled));
28+
clone.appendChild(cloneNode(child, javascriptEnabled));
6729
}
6830
child = child.nextSibling;
6931
}
7032

33+
if (node.nodeType === 1) {
34+
clone._scrollTop = node.scrollTop;
35+
clone._scrollLeft = node.scrollLeft;
36+
if (node.nodeName === "CANVAS") {
37+
cloneCanvasContents(node, clone);
38+
} else if (node.nodeName === "TEXTAREA" || node.nodeName === "SELECT") {
39+
clone.value = node.value;
40+
}
41+
}
42+
7143
return clone;
7244
}
7345

46+
function initNode(node) {
47+
if (node.nodeType === 1) {
48+
node.scrollTop = node._scrollTop;
49+
node.scrollLeft = node._scrollLeft;
7450

75-
76-
function isElementNode(node) {
77-
return node.nodeType === Node.ELEMENT_NODE;
51+
var child = node.firstChild;
52+
while(child) {
53+
initNode(child);
54+
child = child.nextSibling;
55+
}
56+
}
7857
}
7958

8059
module.exports = function(ownerDocument, containerDocument, width, height, options, x ,y) {
81-
labelCanvasElements(ownerDocument);
82-
var documentElement = isIE9() ? cloneNodeIE9(ownerDocument.documentElement, options.javascriptEnabled) : ownerDocument.documentElement.cloneNode(true);
60+
var documentElement = cloneNode(ownerDocument.documentElement, options.javascriptEnabled);
8361
var container = containerDocument.createElement("iframe");
8462

8563
container.className = "html2canvas-container";
@@ -96,16 +74,13 @@ module.exports = function(ownerDocument, containerDocument, width, height, optio
9674
return new Promise(function(resolve) {
9775
var documentClone = container.contentWindow.document;
9876

99-
cloneNodeValues(ownerDocument.documentElement, documentElement, "textarea");
100-
cloneNodeValues(ownerDocument.documentElement, documentElement, "select");
101-
10277
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
10378
if window url is about:blank, we can assign the url to current by writing onto the document
10479
*/
10580
container.contentWindow.onload = container.onload = function() {
10681
var interval = setInterval(function() {
10782
if (documentClone.body.childNodes.length > 0) {
108-
cloneCanvasContents(ownerDocument, documentClone);
83+
initNode(documentClone.documentElement);
10984
clearInterval(interval);
11085
if (options.type === "view") {
11186
container.contentWindow.scrollTo(x, y);
@@ -119,7 +94,7 @@ module.exports = function(ownerDocument, containerDocument, width, height, optio
11994
documentClone.write("<!DOCTYPE html><html></html>");
12095
// Chrome scrolls the parent document for some reason after the write to the cloned window???
12196
restoreOwnerScroll(ownerDocument, x, y);
122-
documentClone.replaceChild(options.javascriptEnabled === true ? documentClone.adoptNode(documentElement) : removeScriptNodes(documentClone.adoptNode(documentElement)), documentClone.documentElement);
97+
documentClone.replaceChild(documentClone.adoptNode(documentElement), documentClone.documentElement);
12398
documentClone.close();
12499
});
125100
};

tests/mocha/form-rendering.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
CanvasRenderer.prototype.text = function(text) {
109109
expect(text).to.equal('updated');
110110
};
111-
html2canvas(document.querySelector("#block4"), {renderer: CanvasRenderer, strict: true, logging: true, removeContainer: false}).then(function(canvas) {
111+
html2canvas(document.querySelector("#block4"), {renderer: CanvasRenderer, strict: true}).then(function(canvas) {
112112
expect(canvas.width).to.equal(200);
113113
expect(canvas.height).to.equal(200);
114114
done();
@@ -138,7 +138,7 @@
138138
CanvasRenderer.prototype.text = function(text) {
139139
expect(text).to.equal('3');
140140
};
141-
html2canvas(document.querySelector("#block5"), {renderer: CanvasRenderer, strict: true, logging: true, removeContainer: false}).then(function(canvas) {
141+
html2canvas(document.querySelector("#block5"), {renderer: CanvasRenderer, strict: true}).then(function(canvas) {
142142
expect(canvas.width).to.equal(200);
143143
expect(canvas.height).to.equal(200);
144144
done();

tests/mocha/scrolling.html

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
<body>
1313
<div id="mocha"></div>
1414
<script>mocha.setup('bdd')</script>
15+
<div id="scroll-render" style="height: 200px; width: 200px;">
16+
<div style="height: 500px; width: 400px;overflow: scroll;" id="scrollable">
17+
<div style="height: 500px;background: red;"></div>
18+
<div style="height: 650px; background: green"></div>
19+
</div>
20+
</div>
1521
<div style="height: 2200px;"></div>
1622
<div style="height: 500px;background: green;"><a name="content">content</a></div>
1723
<script>
@@ -32,7 +38,7 @@
3238
window.location.hash = "#content";
3339
setTimeout(function() {
3440
var top = $(window).scrollTop();
35-
html2canvas(document.body, {type: 'view', removeContainer: false}).then(function () {
41+
html2canvas(document.body, {type: 'view'}).then(function () {
3642
var currentTop = $(window).scrollTop();
3743
window.location.hash = "";
3844
expect(currentTop).to.be.greaterThan(1500);
@@ -45,8 +51,35 @@
4551
});
4652
}, 0);
4753
});
54+
55+
it("with content scroll", function (done) {
56+
$("#scrollable").scrollTop(500);
57+
setTimeout(function() {
58+
html2canvas(document.querySelector("#scroll-render")).then(function (canvas) {
59+
expect($("#scrollable").scrollTop()).to.equal(500);
60+
document.body.appendChild(canvas);
61+
expect(canvas.width).to.equal(200);
62+
expect(canvas.height).to.equal(200);
63+
validCanvasPixels(canvas);
64+
done();
65+
}).catch(function (error) {
66+
done(error);
67+
});
68+
}, 0);
69+
});
4870
});
4971

72+
function validCanvasPixels(canvas) {
73+
var ctx = canvas.getContext("2d");
74+
var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
75+
for (var i = 0, len = 200*199*4; i < len; i+=4) {
76+
if (data[i] !== 0 || data[i+1] !== 128 || data[i+2] !== 0 || data[i+3] !== 255) {
77+
console.log(i, data[i], data[i+1], data[i+2], data[i+3]);
78+
expect().fail("Invalid canvas data");
79+
}
80+
}
81+
}
82+
5083
after(function() {
5184
if (history) {
5285
history.pushState("", document.title, window.location.pathname + window.location.search);

0 commit comments

Comments
 (0)