]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add support for detached canvas element (#4591)
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Tue, 1 Aug 2017 12:28:45 +0000 (14:28 +0200)
committerGitHub <noreply@github.com>
Tue, 1 Aug 2017 12:28:45 +0000 (14:28 +0200)
Allow to create a chart on a canvas not yet attached to the DOM (detection based on CSS animations described in https://davidwalsh.name/detect-node-insertion). The resize element (IFRAME) is added only when the canvas receives a parent or when `style.display` changes from `none`. This change also allows to re-parent the canvas under a different node (the resizer element following). This is a preliminary work for the DIV based resizer.

src/chart.js
src/core/core.helpers.js
src/platforms/platform.dom.js
src/platforms/platform.js
test/jasmine.matchers.js
test/specs/core.controller.tests.js

index 9b50728a263a0e841162d4b8fe872293495ca95e..8bd49761461bad0eea9bd3350fdfdbb38c791ff1 100644 (file)
@@ -60,6 +60,8 @@ plugins.push(
 
 Chart.plugins.register(plugins);
 
+Chart.platform.initialize();
+
 module.exports = Chart;
 if (typeof window !== 'undefined') {
        window.Chart = Chart;
index 98452962e2c1b51ecefcc9c92f56c4946ede99e7..31c1cb4b623d2c044b1377e8d1d36226ff4efc22 100644 (file)
@@ -500,6 +500,10 @@ module.exports = function(Chart) {
        };
        helpers.getMaximumWidth = function(domNode) {
                var container = domNode.parentNode;
+               if (!container) {
+                       return domNode.clientWidth;
+               }
+
                var paddingLeft = parseInt(helpers.getStyle(container, 'padding-left'), 10);
                var paddingRight = parseInt(helpers.getStyle(container, 'padding-right'), 10);
                var w = container.clientWidth - paddingLeft - paddingRight;
@@ -508,6 +512,10 @@ module.exports = function(Chart) {
        };
        helpers.getMaximumHeight = function(domNode) {
                var container = domNode.parentNode;
+               if (!container) {
+                       return domNode.clientHeight;
+               }
+
                var paddingTop = parseInt(helpers.getStyle(container, 'padding-top'), 10);
                var paddingBottom = parseInt(helpers.getStyle(container, 'padding-bottom'), 10);
                var h = container.clientHeight - paddingTop - paddingBottom;
index 25cd1551d3def9e1e1260cd2bfd71da3bb6e4400..2659ac6a43326de892ebfcec70c47625a30c0aa7 100644 (file)
@@ -6,19 +6,21 @@
 
 var helpers = require('../helpers/index');
 
+var EXPANDO_KEY = '$chartjs';
+var CSS_PREFIX = 'chartjs-';
+var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
+var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
+var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];
+
 /**
  * DOM event types -> Chart.js event types.
  * Note: only events with different types are mapped.
  * @see https://developer.mozilla.org/en-US/docs/Web/Events
  */
-
-var eventTypeMap = {
-       // Touch events
+var EVENT_TYPES = {
        touchstart: 'mousedown',
        touchmove: 'mousemove',
        touchend: 'mouseup',
-
-       // Pointer events
        pointerenter: 'mouseenter',
        pointerdown: 'mousedown',
        pointermove: 'mousemove',
@@ -56,7 +58,7 @@ function initCanvas(canvas, config) {
        var renderWidth = canvas.getAttribute('width');
 
        // Chart.js modifies some canvas values that we want to restore on destroy
-       canvas._chartjs = {
+       canvas[EXPANDO_KEY] = {
                initial: {
                        height: renderHeight,
                        width: renderWidth,
@@ -140,11 +142,29 @@ function createEvent(type, chart, x, y, nativeEvent) {
 }
 
 function fromNativeEvent(event, chart) {
-       var type = eventTypeMap[event.type] || event.type;
+       var type = EVENT_TYPES[event.type] || event.type;
        var pos = helpers.getRelativePosition(event, chart);
        return createEvent(type, chart, pos.x, pos.y, event);
 }
 
+function throttled(fn, thisArg) {
+       var ticking = false;
+       var args = [];
+
+       return function() {
+               args = Array.prototype.slice.call(arguments);
+               thisArg = thisArg || this;
+
+               if (!ticking) {
+                       ticking = true;
+                       helpers.requestAnimFrame.call(window, function() {
+                               ticking = false;
+                               fn.apply(thisArg, args);
+                       });
+               }
+       };
+}
+
 function createResizer(handler) {
        var iframe = document.createElement('iframe');
        iframe.className = 'chartjs-hidden-iframe';
@@ -176,7 +196,6 @@ function createResizer(handler) {
        // https://github.com/chartjs/Chart.js/issues/3521
        addEventListener(iframe, 'load', function() {
                addEventListener(iframe.contentWindow || iframe, 'resize', handler);
-
                // The iframe size might have changed while loading, which can also
                // happen if the size has been changed while detached from the DOM.
                handler();
@@ -185,45 +204,100 @@ function createResizer(handler) {
        return iframe;
 }
 
-function addResizeListener(node, listener, chart) {
-       var stub = node._chartjs = {
-               ticking: false
-       };
-
-       // Throttle the callback notification until the next animation frame.
-       var notify = function() {
-               if (!stub.ticking) {
-                       stub.ticking = true;
-                       helpers.requestAnimFrame.call(window, function() {
-                               if (stub.resizer) {
-                                       stub.ticking = false;
-                                       return listener(createEvent('resize', chart));
-                               }
-                       });
+// https://davidwalsh.name/detect-node-insertion
+function watchForRender(node, handler) {
+       var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
+       var proxy = expando.renderProxy = function(e) {
+               if (e.animationName === CSS_RENDER_ANIMATION) {
+                       handler();
                }
        };
 
-       // Let's keep track of this added iframe and thus avoid DOM query when removing it.
-       stub.resizer = createResizer(notify);
+       helpers.each(ANIMATION_START_EVENTS, function(type) {
+               addEventListener(node, type, proxy);
+       });
 
-       node.insertBefore(stub.resizer, node.firstChild);
+       node.classList.add(CSS_RENDER_MONITOR);
 }
 
-function removeResizeListener(node) {
-       if (!node || !node._chartjs) {
-               return;
+function unwatchForRender(node) {
+       var expando = node[EXPANDO_KEY] || {};
+       var proxy = expando.renderProxy;
+
+       if (proxy) {
+               helpers.each(ANIMATION_START_EVENTS, function(type) {
+                       removeEventListener(node, type, proxy);
+               });
+
+               delete expando.renderProxy;
        }
 
-       var resizer = node._chartjs.resizer;
-       if (resizer) {
+       node.classList.remove(CSS_RENDER_MONITOR);
+}
+
+function addResizeListener(node, listener, chart) {
+       var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
+
+       // Let's keep track of this added resizer and thus avoid DOM query when removing it.
+       var resizer = expando.resizer = createResizer(throttled(function() {
+               if (expando.resizer) {
+                       return listener(createEvent('resize', chart));
+               }
+       }));
+
+       // The resizer needs to be attached to the node parent, so we first need to be
+       // sure that `node` is attached to the DOM before injecting the resizer element.
+       watchForRender(node, function() {
+               if (expando.resizer) {
+                       var container = node.parentNode;
+                       if (container && container !== resizer.parentNode) {
+                               container.insertBefore(resizer, container.firstChild);
+                       }
+               }
+       });
+}
+
+function removeResizeListener(node) {
+       var expando = node[EXPANDO_KEY] || {};
+       var resizer = expando.resizer;
+
+       delete expando.resizer;
+       unwatchForRender(node);
+
+       if (resizer && resizer.parentNode) {
                resizer.parentNode.removeChild(resizer);
-               node._chartjs.resizer = null;
+       }
+}
+
+function injectCSS(platform, css) {
+       // http://stackoverflow.com/q/3922139
+       var style = platform._style || document.createElement('style');
+       if (!platform._style) {
+               platform._style = style;
+               css = '/* Chart.js */\n' + css;
+               style.setAttribute('type', 'text/css');
+               document.getElementsByTagName('head')[0].appendChild(style);
        }
 
-       delete node._chartjs;
+       style.appendChild(document.createTextNode(css));
 }
 
 module.exports = {
+       initialize: function() {
+               var keyframes = 'from{opacity:0.99}to{opacity:1}';
+
+               injectCSS(this,
+                       // DOM rendering detection
+                       // https://davidwalsh.name/detect-node-insertion
+                       '@-webkit-keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
+                       '@keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
+                       '.' + CSS_RENDER_MONITOR + '{' +
+                               '-webkit-animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
+                               'animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
+                       '}'
+               );
+       },
+
        acquireContext: function(item, config) {
                if (typeof item === 'string') {
                        item = document.getElementById(item);
@@ -259,11 +333,11 @@ module.exports = {
 
        releaseContext: function(context) {
                var canvas = context.canvas;
-               if (!canvas._chartjs) {
+               if (!canvas[EXPANDO_KEY]) {
                        return;
                }
 
-               var initial = canvas._chartjs.initial;
+               var initial = canvas[EXPANDO_KEY].initial;
                ['height', 'width'].forEach(function(prop) {
                        var value = initial[prop];
                        if (helpers.isNullOrUndef(value)) {
@@ -283,19 +357,19 @@ module.exports = {
                // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html
                canvas.width = canvas.width;
 
-               delete canvas._chartjs;
+               delete canvas[EXPANDO_KEY];
        },
 
        addEventListener: function(chart, type, listener) {
                var canvas = chart.canvas;
                if (type === 'resize') {
                        // Note: the resize event is not supported on all browsers.
-                       addResizeListener(canvas.parentNode, listener, chart);
+                       addResizeListener(canvas, listener, chart);
                        return;
                }
 
-               var stub = listener._chartjs || (listener._chartjs = {});
-               var proxies = stub.proxies || (stub.proxies = {});
+               var expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {});
+               var proxies = expando.proxies || (expando.proxies = {});
                var proxy = proxies[chart.id + '_' + type] = function(event) {
                        listener(fromNativeEvent(event, chart));
                };
@@ -307,12 +381,12 @@ module.exports = {
                var canvas = chart.canvas;
                if (type === 'resize') {
                        // Note: the resize event is not supported on all browsers.
-                       removeResizeListener(canvas.parentNode, listener);
+                       removeResizeListener(canvas, listener);
                        return;
                }
 
-               var stub = listener._chartjs || {};
-               var proxies = stub.proxies || {};
+               var expando = listener[EXPANDO_KEY] || {};
+               var proxies = expando.proxies || {};
                var proxy = proxies[chart.id + '_' + type];
                if (!proxy) {
                        return;
index 199e9548dbf8e008c142868d2b81d6e2e4ca3231..8f4827732b1bf0502d6084c486288a1e11b37759 100644 (file)
@@ -12,6 +12,11 @@ var implementation = require('./platform.dom');
  * @since 2.4.0
  */
 module.exports = helpers.extend({
+       /**
+        * @since 2.7.0
+        */
+       initialize: function() {},
+
        /**
         * Called at chart construction time, returns a context2d instance implementing
         * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.
index f216e1e1c019688a4f144031d041a902cd3a3260..88f5de8fe7e8a2c2235883311d7092f838ce435a 100644 (file)
@@ -123,8 +123,8 @@ function toBeChartOfSize() {
                        var canvas = actual.ctx.canvas;
                        var style = getComputedStyle(canvas);
                        var pixelRatio = actual.options.devicePixelRatio || window.devicePixelRatio;
-                       var dh = parseInt(style.height, 10);
-                       var dw = parseInt(style.width, 10);
+                       var dh = parseInt(style.height, 10) || 0;
+                       var dw = parseInt(style.width, 10) || 0;
                        var rh = canvas.height;
                        var rw = canvas.width;
                        var orh = rh / pixelRatio;
index 4c6e61858651c7295e51b9fccd38b1b00810833f..24f062b78d3abc2625072a5517640c6adc6b785e 100644 (file)
@@ -363,6 +363,90 @@ describe('Chart', function() {
                        });
                });
 
+               // https://github.com/chartjs/Chart.js/issues/3790
+               it('should resize the canvas if attached to the DOM after construction', function(done) {
+                       var canvas = document.createElement('canvas');
+                       var wrapper = document.createElement('div');
+                       var body = window.document.body;
+                       var chart = new Chart(canvas, {
+                               type: 'line',
+                               options: {
+                                       responsive: true,
+                                       maintainAspectRatio: false
+                               }
+                       });
+
+                       expect(chart).toBeChartOfSize({
+                               dw: 0, dh: 0,
+                               rw: 0, rh: 0,
+                       });
+
+                       wrapper.style.cssText = 'width: 455px; height: 355px';
+                       wrapper.appendChild(canvas);
+                       body.appendChild(wrapper);
+
+                       waitForResize(chart, function() {
+                               expect(chart).toBeChartOfSize({
+                                       dw: 455, dh: 355,
+                                       rw: 455, rh: 355,
+                               });
+
+                               body.removeChild(wrapper);
+                               chart.destroy();
+                               done();
+                       });
+               });
+
+               it('should resize the canvas when attached to a different parent', function(done) {
+                       var canvas = document.createElement('canvas');
+                       var wrapper = document.createElement('div');
+                       var body = window.document.body;
+                       var chart = new Chart(canvas, {
+                               type: 'line',
+                               options: {
+                                       responsive: true,
+                                       maintainAspectRatio: false
+                               }
+                       });
+
+                       expect(chart).toBeChartOfSize({
+                               dw: 0, dh: 0,
+                               rw: 0, rh: 0,
+                       });
+
+                       wrapper.style.cssText = 'width: 455px; height: 355px';
+                       wrapper.appendChild(canvas);
+                       body.appendChild(wrapper);
+
+                       waitForResize(chart, function() {
+                               var resizer = wrapper.firstChild;
+                               expect(resizer.tagName).toBe('IFRAME');
+                               expect(chart).toBeChartOfSize({
+                                       dw: 455, dh: 355,
+                                       rw: 455, rh: 355,
+                               });
+
+                               var target = document.createElement('div');
+                               target.style.cssText = 'width: 640px; height: 480px';
+                               target.appendChild(canvas);
+                               body.appendChild(target);
+
+                               waitForResize(chart, function() {
+                                       expect(target.firstChild).toBe(resizer);
+                                       expect(wrapper.firstChild).toBe(null);
+                                       expect(chart).toBeChartOfSize({
+                                               dw: 640, dh: 480,
+                                               rw: 640, rh: 480,
+                                       });
+
+                                       body.removeChild(wrapper);
+                                       body.removeChild(target);
+                                       chart.destroy();
+                                       done();
+                               });
+                       });
+               });
+
                // https://github.com/chartjs/Chart.js/issues/3521
                it('should resize the canvas after the wrapper has been re-attached to the DOM', function(done) {
                        var chart = acquireChart({
@@ -592,23 +676,26 @@ describe('Chart', function() {
        });
 
        describe('controller.destroy', function() {
-               it('should remove the resizer element when responsive: true', function() {
+               it('should remove the resizer element when responsive: true', function(done) {
                        var chart = acquireChart({
                                options: {
                                        responsive: true
                                }
                        });
 
-                       var wrapper = chart.canvas.parentNode;
-                       var resizer = wrapper.firstChild;
+                       waitForResize(chart, function() {
+                               var wrapper = chart.canvas.parentNode;
+                               var resizer = wrapper.firstChild;
+                               expect(wrapper.childNodes.length).toBe(2);
+                               expect(resizer.tagName).toBe('IFRAME');
 
-                       expect(wrapper.childNodes.length).toBe(2);
-                       expect(resizer.tagName).toBe('IFRAME');
+                               chart.destroy();
 
-                       chart.destroy();
+                               expect(wrapper.childNodes.length).toBe(1);
+                               expect(wrapper.firstChild.tagName).toBe('CANVAS');
 
-                       expect(wrapper.childNodes.length).toBe(1);
-                       expect(wrapper.firstChild.tagName).toBe('CANVAS');
+                               done();
+                       });
                });
        });