diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 9d7983887b3..4162ac8f198 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -123,7 +123,7 @@ class Chart { this.attached = false; this._animationsDisabled = undefined; this.$context = undefined; - this._doResize = debounce(() => this.update('resize'), options.resizeDelay || 0); + this._doResize = debounce(mode => this.update(mode), options.resizeDelay || 0); // Add the chart instance to the global namespace instances[me.id] = me; @@ -231,6 +231,7 @@ class Chart { const aspectRatio = options.maintainAspectRatio && me.aspectRatio; const newSize = me.platform.getMaximumSize(canvas, width, height, aspectRatio); const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio(); + const mode = me.width ? 'resize' : 'attach'; me.width = newSize.width; me.height = newSize.height; @@ -244,7 +245,7 @@ class Chart { callCallback(options.onResize, [me, newSize], me); if (me.attached) { - if (me._doResize()) { + if (me._doResize(mode)) { // The resize update is delayed, only draw without updating. me.render(); } @@ -831,19 +832,22 @@ class Chart { } } - destroy() { + _stop() { const me = this; - const {canvas, ctx} = me; let i, ilen; - me.stop(); animator.remove(me); - // dataset controllers need to cleanup associated data for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { me._destroyDatasetMeta(i); } + } + + destroy() { + const me = this; + const {canvas, ctx} = me; + me._stop(); me.config.clearCache(); if (canvas) { @@ -940,6 +944,11 @@ class Chart { me.attached = false; _remove('resize', listener); + + // Stop animating and remove metasets, so when re-attached, the animations start from begining. + me._stop(); + me._resize(0, 0); + _add('attach', attached); }; diff --git a/src/helpers/helpers.extras.js b/src/helpers/helpers.extras.js index 09739ce8671..fd7608b9cc5 100644 --- a/src/helpers/helpers.extras.js +++ b/src/helpers/helpers.extras.js @@ -42,24 +42,23 @@ export function throttled(fn, thisArg, updateFn) { /** * Debounces calling `fn` for `delay` ms - * @param {function} fn - Function to call. No arguments are passed. + * @param {function} fn - Function to call. * @param {number} delay - Delay in ms. 0 = immediate invocation. * @returns {function} */ export function debounce(fn, delay) { let timeout; - return function() { + return function(...args) { if (delay) { clearTimeout(timeout); - timeout = setTimeout(fn, delay); + timeout = setTimeout(fn, delay, args); } else { - fn(); + fn.apply(this, args); } return delay; }; } - /** * Converts 'start' to 'left', 'end' to 'right' and others to 'center' * @param {string} align start, end, center diff --git a/src/platform/platform.dom.js b/src/platform/platform.dom.js index 8e7858f750a..03844deea81 100644 --- a/src/platform/platform.dom.js +++ b/src/platform/platform.dom.js @@ -116,18 +116,14 @@ function fromNativeEvent(event, chart) { function createAttachObserver(chart, type, listener) { const canvas = chart.canvas; - const container = canvas && _getParentNode(canvas); - const element = container || canvas; const observer = new MutationObserver(entries => { - const parent = _getParentNode(element); - entries.forEach(entry => { - for (let i = 0; i < entry.addedNodes.length; i++) { - const added = entry.addedNodes[i]; - if (added === element || added === parent) { - listener(entry.target); + for (const entry of entries) { + for (const node of entry.addedNodes) { + if (node === canvas || node.contains(canvas)) { + return listener(); } } - }); + } }); observer.observe(document, {childList: true, subtree: true}); return observer; @@ -135,21 +131,16 @@ function createAttachObserver(chart, type, listener) { function createDetachObserver(chart, type, listener) { const canvas = chart.canvas; - const container = canvas && _getParentNode(canvas); - if (!container) { - return; - } const observer = new MutationObserver(entries => { - entries.forEach(entry => { - for (let i = 0; i < entry.removedNodes.length; i++) { - if (entry.removedNodes[i] === canvas) { - listener(); - break; + for (const entry of entries) { + for (const node of entry.removedNodes) { + if (node === canvas || node.contains(canvas)) { + return listener(); } } - }); + } }); - observer.observe(container, {childList: true}); + observer.observe(document, {childList: true, subtree: true}); return observer; } diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 5072537e975..809b721d9d4 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -1029,8 +1029,10 @@ describe('Chart', function() { }); parent.removeChild(wrapper); - parent.appendChild(wrapper); - wrapper.style.height = '355px'; + setTimeout(() => { + parent.appendChild(wrapper); + wrapper.style.height = '355px'; + }, 0); }); // https://github.com/chartjs/Chart.js/issues/4737 @@ -1075,6 +1077,47 @@ describe('Chart', function() { canvas.parentNode.style.width = '455px'; }); }); + + it('should resize the canvas if attached to the DOM after construction with mutiple parents', function(done) { + var canvas = document.createElement('canvas'); + var wrapper = document.createElement('div'); + var wrapper2 = document.createElement('div'); + var wrapper3 = 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, + }); + expect(chart.chartArea).toBeUndefined(); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + expect(chart.chartArea).not.toBeUndefined(); + + body.removeChild(wrapper3); + chart.destroy(); + done(); + }); + + wrapper3.appendChild(wrapper2); + wrapper2.appendChild(wrapper); + wrapper.style.cssText = 'width: 455px; height: 355px'; + wrapper.appendChild(canvas); + body.appendChild(wrapper3); + }); }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() {