]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Use ResizeObserver and MutationObserver to detect detach/attach/resize (#7104)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Mon, 17 Feb 2020 16:00:03 +0000 (18:00 +0200)
committerGitHub <noreply@github.com>
Mon, 17 Feb 2020 16:00:03 +0000 (11:00 -0500)
* Use Resize/MutationObserver to detect detach/attach/resize
* Cleanup
* Review update
* Restore infinite resize detection (#6011)

17 files changed:
docs/getting-started/integration.md
docs/getting-started/v3-migration.md
package-lock.json
package.json
rollup.config.js
rollup.plugins.js
samples/advanced/content-security-policy.css [deleted file]
samples/advanced/content-security-policy.html [deleted file]
samples/advanced/content-security-policy.js [deleted file]
samples/samples.js
src/core/core.controller.js
src/helpers/helpers.dom.js
src/index.js
src/platform/platform.dom.css [deleted file]
src/platform/platform.dom.js
src/platform/platform.js [deleted file]
test/specs/core.controller.tests.js

index 83301191a32d984eee6fbc0ab84a3342d76afba8..049e23a56b2ef2a7ce9f279f1a7c021acf904fc4 100644 (file)
@@ -64,18 +64,3 @@ require(['moment'], function() {
     });
 });
 ```
-
-## Content Security Policy
-
-By default, Chart.js injects CSS directly into the DOM. For webpages secured using [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), this requires to allow `style-src 'unsafe-inline'`. For stricter CSP environments, where only `style-src 'self'` is allowed, the following CSS file needs to be manually added to your webpage:
-
-```html
-<link rel="stylesheet" type="text/css" href="path/to/chartjs/dist/Chart.min.css">
-```
-
-And the style injection must be turned off **before creating the first chart**:
-
-```javascript
-// Disable automatic style injection
-Chart.platform.disableCSSInjection = true;
-```
index c8d239d02dd2f8d34c0beefeb71026f66820a47f..0aee422ab90cf3cefcbbaee09c08aefae52aa575 100644 (file)
@@ -7,6 +7,7 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released
 * Completely rewritten animation system
 * Rewritten filler plugin with numerous bug fixes
 * API Documentation generated and verified by TypeScript
+* No more CSS injection
 * Tons of bug fixes
 
 ## End user migration
@@ -88,6 +89,7 @@ Animation system was completely rewritten in Chart.js v3. Each property can now
 * `Chart.chart.chart`
 * `Chart.Controller`
 * `Chart.prototype.generateLegend`
+* `Chart.platform`. It only contained `disableCSSInjection`. CSS is never injected in v3.
 * `Chart.types`
 * `Chart.Tooltip` is now provided by the tooltip plugin. The positioners can be accessed from `tooltipPlugin.positioners`
 * `DatasetController.addElementAndReset`
@@ -253,6 +255,6 @@ Animation system was completely rewritten in Chart.js v3. Each property can now
 
 #### Platform
 
-* `Chart.platform` is no longer the platform object used by charts. It contains only a single configuration option, `disableCSSInjection`. Every chart instance now has a separate platform instance.
+* `Chart.platform` is no longer the platform object used by charts. Every chart instance now has a separate platform instance.
 * `Chart.platforms` is an object that contains two usable platform classes, `BasicPlatform` and `DomPlatform`. It also contains `BasePlatform`, a class that all platforms must extend from.
 * If the canvas passed in is an instance of `OffscreenCanvas`, the `BasicPlatform` is automatically used.
index 5347b531a1f8702b99da405e2062f53115276fad..333eba827fd74225a1e42de01d169ccb5343df14 100644 (file)
         }
       }
     },
-    "clean-css": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
-      "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
-      "dev": true,
-      "requires": {
-        "source-map": "~0.6.0"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        }
-      }
-    },
     "cli-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
       "dev": true
     },
+    "resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "dev": true
+    },
     "resolve": {
       "version": "1.12.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
index a66cdac916bc5718147b8ae65f2fd1c647a7d427..6a0dc260954f3824d8f7c626e47a80a61e2f0503 100644 (file)
@@ -34,7 +34,6 @@
     "@babel/plugin-transform-object-assign": "^7.8.3",
     "@babel/preset-env": "^7.8.4",
     "babel-preset-es2015-rollup": "^3.0.0",
-    "clean-css": "^4.2.3",
     "coveralls": "^3.0.9",
     "eslint": "^6.8.0",
     "eslint-config-chartjs": "^0.2.0",
@@ -66,6 +65,7 @@
     "merge-stream": "^1.0.1",
     "moment": "^2.10.2",
     "pixelmatch": "^5.0.0",
+    "resize-observer-polyfill": "^1.5.1",
     "rollup": "^1.31.0",
     "rollup-plugin-babel": "^4.3.3",
     "rollup-plugin-cleanup": "^3.1.1",
index 459de3ce69d8df328346628360762683b4877ed9..9054c5657f169ce1b65574632a3491ed30909dfe 100644 (file)
@@ -7,7 +7,6 @@ const babel = require('rollup-plugin-babel');
 const cleanup = require('rollup-plugin-cleanup');
 const terser = require('rollup-plugin-terser').terser;
 const optional = require('./rollup.plugins').optional;
-const stylesheet = require('./rollup.plugins').stylesheet;
 const pkg = require('./package.json');
 
 const input = 'src/index.js';
@@ -29,13 +28,12 @@ module.exports = [
                        resolve(),
                        commonjs(),
                        babel(),
-                       stylesheet({
-                               extract: true
-                       }),
                        optional({
                                include: ['moment']
                        }),
-                       cleanup(),
+                       cleanup({
+                               sourcemap: true
+                       })
                ],
                output: {
                        name: 'Chart',
@@ -60,10 +58,6 @@ module.exports = [
                        optional({
                                include: ['moment']
                        }),
-                       stylesheet({
-                               extract: true,
-                               minify: true
-                       }),
                        terser({
                                output: {
                                        preamble: banner
@@ -93,10 +87,9 @@ module.exports = [
                        resolve(),
                        commonjs(),
                        babel(),
-                       stylesheet({
-                               extract: true
-                       }),
-                       cleanup(),
+                       cleanup({
+                               sourcemap: true
+                       })
                ],
                output: {
                        name: 'Chart',
@@ -118,10 +111,6 @@ module.exports = [
                        resolve(),
                        commonjs(),
                        babel(),
-                       stylesheet({
-                               extract: true,
-                               minify: true
-                       }),
                        terser({
                                output: {
                                        preamble: banner
index 8c8dd9624c1729859b204b162c92a8ff0e53e1e5..0a8598d124d6c40788e1252bfeabd8215b4d9836 100644 (file)
@@ -1,7 +1,4 @@
-/* eslint-env es6 */
-const cleancss = require('clean-css');
-const path = require('path');
-
+/* eslint-disable import/no-commonjs */
 const UMD_WRAPPER_RE = /(\(function \(global, factory\) \{)((?:\s.*?)*)(\}\(this,)/;
 const CJS_FACTORY_RE = /(module.exports = )(factory\(.*?\))( :)/;
 const AMD_FACTORY_RE = /(define\()(.*?, factory)(\) :)/;
@@ -24,7 +21,7 @@ function optional(config = {}) {
                        let factory = (CJS_FACTORY_RE.exec(content) || [])[2];
                        let updated = false;
 
-                       for (let lib of chunk.imports) {
+                       for (const lib of chunk.imports) {
                                if (!include || include.indexOf(lib) !== -1) {
                                        const regex = new RegExp(`require\\('${lib}'\\)`);
                                        if (!regex.test(factory)) {
@@ -58,53 +55,6 @@ function optional(config = {}) {
        };
 }
 
-// https://github.com/chartjs/Chart.js/issues/5208
-function stylesheet(config = {}) {
-       const minifier = new cleancss();
-       const styles = [];
-
-       return {
-               name: 'stylesheet',
-               transform(code, id) {
-                       // Note that 'id' can be mapped to a CJS proxy import, in which case
-                       // 'id' will start with 'commonjs-proxy', so let's first check if we
-                       // are importing an existing css file (i.e. startsWith()).
-                       if (!id.startsWith(path.resolve('.')) || !id.endsWith('.css')) {
-                               return;
-                       }
-
-                       if (config.minify) {
-                               code = minifier.minify(code).styles;
-                       }
-
-                       // keep track of all imported stylesheets (already minified)
-                       styles.push(code);
-
-                       return {
-                               code: 'export default ' + JSON.stringify(code)
-                       };
-               },
-               generateBundle(opts, bundle) {
-                       if (!config.extract) {
-                               return;
-                       }
-
-                       const entry = Object.keys(bundle).find(v => bundle[v].isEntry);
-                       const name = (entry || '').replace(/\.js$/i, '.css');
-                       if (!name) {
-                               this.error('failed to guess the output file name');
-                       }
-
-                       this.emitFile({
-                               type: 'asset',
-                               source: styles.filter(v => !!v).join(''),
-                               fileName: name
-                       });
-               }
-       };
-}
-
 module.exports = {
-       optional,
-       stylesheet
+       optional
 };
diff --git a/samples/advanced/content-security-policy.css b/samples/advanced/content-security-policy.css
deleted file mode 100644 (file)
index 8e5b8fd..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-.content {
-       max-width: 640px;
-       margin: auto;
-       padding: 1rem;
-}
-
-.note {
-       font-family: sans-serif;
-       color: #5050a0;
-       line-height: 1.4;
-       margin-bottom: 1rem;
-       padding: 1rem;
-}
-
-code {
-       background-color: #f5f5ff;
-       border: 1px solid #d0d0fa;
-       border-radius: 4px;
-       padding: 0.05rem 0.25rem;
-}
diff --git a/samples/advanced/content-security-policy.html b/samples/advanced/content-security-policy.html
deleted file mode 100644 (file)
index fb2805c..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html lang="en-US">
-<head>
-       <meta charset="utf-8">
-       <meta http-equiv="X-UA-Compatible" content="IE=Edge">
-       <meta name="viewport" content="width=device-width, initial-scale=1">
-       <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
-       <title>Scriptable > Bubble | Chart.js sample</title>
-       <link rel="stylesheet" type="text/css" href="../../dist/Chart.min.css">
-       <link rel="stylesheet" type="text/css" href="./content-security-policy.css">
-       <script src="../../dist/Chart.min.js"></script>
-       <script src="../utils.js"></script>
-       <script src="content-security-policy.js"></script>
-</head>
-<body>
-       <div class="content">
-               <div class="note">
-                       In order to support a strict content security policy (<code>default-src 'self'</code>),
-                       this page manually loads <code>Chart.min.css</code> and turns off the automatic style
-                       injection by setting <code>Chart.platform.disableCSSInjection = true;</code>.
-               </div>
-               <div class="wrapper">
-                       <canvas id="chart-0"></canvas>
-               </div>
-       </div>
-</body>
-</html>
diff --git a/samples/advanced/content-security-policy.js b/samples/advanced/content-security-policy.js
deleted file mode 100644 (file)
index a974fc6..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-var utils = Samples.utils;
-
-utils.srand(110);
-// CSP: disable automatic style injection
-Chart.platform.disableCSSInjection = true;
-
-function generateData() {
-       var DATA_COUNT = 16;
-       var MIN_XY = -150;
-       var MAX_XY = 100;
-       var data = [];
-       var i;
-
-       for (i = 0; i < DATA_COUNT; ++i) {
-               data.push({
-                       x: utils.rand(MIN_XY, MAX_XY),
-                       y: utils.rand(MIN_XY, MAX_XY),
-                       v: utils.rand(0, 1000)
-               });
-       }
-
-       return data;
-}
-
-window.addEventListener('load', function() {
-       new Chart('chart-0', {
-               type: 'bubble',
-               data: {
-                       datasets: [{
-                               backgroundColor: utils.color(0),
-                               data: generateData()
-                       }, {
-                               backgroundColor: utils.color(1),
-                               data: generateData()
-                       }]
-               },
-               options: {
-                       aspectRatio: 1,
-                       legend: false,
-                       tooltip: false,
-                       elements: {
-                               point: {
-                                       radius: function(context) {
-                                               var value = context.dataset.data[context.dataIndex];
-                                               var size = context.chart.width;
-                                               var base = Math.abs(value.v) / 1000;
-                                               return (size / 24) * base;
-                                       }
-                               }
-                       }
-               }
-       });
-});
index 7811c8690d4f0232b8f1bb542cdf131f530569de..1f4fc37708f50a0c12bcd2147ad177b866545d2c 100644 (file)
                items: [{
                        title: 'Progress bar',
                        path: 'advanced/progress-bar.html'
-               }, {
-                       title: 'Content Security Policy',
-                       path: 'advanced/content-security-policy.html'
                }, {
                        title: 'Polar Area Radial Gradient',
                        path: 'advanced/radial-gradient.html'
index aca27dc759cb6a4208102fe9418714e402391fc1..c3a29d5f22c1b863106cf2481331f01ff64a16d7 100644 (file)
@@ -7,6 +7,7 @@ import layouts from './core.layouts';
 import {BasicPlatform, DomPlatform} from '../platform/platforms';
 import plugins from './core.plugins';
 import scaleService from '../core/core.scaleService';
+import {getMaximumWidth, getMaximumHeight} from '../helpers/helpers.dom';
 
 /**
  * @typedef { import("../platform/platform.base").IEvent } IEvent
@@ -207,6 +208,7 @@ class Chart {
                this.scales = {};
                this.scale = undefined;
                this.$plugins = undefined;
+               this.$proxies = {};
 
                // Add the chart instance to the global namespace
                Chart.instances[me.id] = me;
@@ -246,15 +248,15 @@ class Chart {
                // Before init plugin notification
                plugins.notify(me, 'beforeInit');
 
-               helpers.dom.retinaScale(me, me.options.devicePixelRatio);
-
-               me.bindEvents();
-
                if (me.options.responsive) {
                        // Initial resize before chart draws (must be silent to preserve initial animations).
                        me.resize(true);
+               } else {
+                       helpers.dom.retinaScale(me, me.options.devicePixelRatio);
                }
 
+               me.bindEvents();
+
                // After init plugin notification
                plugins.notify(me, 'afterInit');
 
@@ -285,19 +287,25 @@ class Chart {
                return this;
        }
 
-       resize(silent) {
+       resize(silent, width, height) {
                const me = this;
                const options = me.options;
                const canvas = me.canvas;
-               const aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null;
-               const oldRatio = me.currentDevicePixelRatio;
+               const aspectRatio = options.maintainAspectRatio && me.aspectRatio;
 
+               if (width === undefined || height === undefined) {
+                       width = getMaximumWidth(canvas);
+                       height = getMaximumHeight(canvas);
+               }
                // the canvas render width and height will be casted to integers so make sure that
                // the canvas display style uses the same integer values to avoid blurring effect.
 
                // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed
-               const newWidth = Math.max(0, Math.floor(helpers.dom.getMaximumWidth(canvas)));
-               const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.dom.getMaximumHeight(canvas)));
+               const newWidth = Math.max(0, Math.floor(width));
+               const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : height));
+
+               // detect devicePixelRation changes
+               const oldRatio = me.currentDevicePixelRatio;
                const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio();
 
                if (me.width === newWidth && me.height === newHeight && oldRatio === newRatio) {
@@ -932,11 +940,11 @@ class Chart {
                        listeners[type] = listener;
                });
 
-               // Elements used to detect size change should not be injected for non responsive charts.
-               // See https://github.com/chartjs/Chart.js/issues/2210
                if (me.options.responsive) {
-                       listener = function() {
-                               me.resize();
+                       listener = function(width, height) {
+                               if (me.canvas) {
+                                       me.resize(false, width, height);
+                               }
                        };
 
                        me.platform.addEventListener(me, 'resize', listener);
index 86114ff503ebed0339ebe134f360b355b89d8bb1..e71ff3352d30f41d734f2f113e9bf4773698dbe7 100644 (file)
@@ -9,7 +9,7 @@ function isConstrainedValue(value) {
 /**
  * @private
  */
-function _getParentNode(domNode) {
+export function _getParentNode(domNode) {
        let parent = domNode.parentNode;
        if (parent && parent.toString() === '[object ShadowRoot]') {
                parent = parent.host;
index 695ae4fe9a3e4851e7ebd27f9ec44435149a6de7..d1b19287a79f356204d182005ada10d49780454e 100644 (file)
@@ -16,7 +16,6 @@ import elements from './elements/index';
 import Interaction from './core/core.interaction';
 import layouts from './core/core.layouts';
 import platforms from './platform/platforms';
-import platform from './platform/platform';
 import pluginsCore from './core/core.plugins';
 import Scale from './core/core.scale';
 import scaleService from './core/core.scaleService';
@@ -35,7 +34,6 @@ Chart.elements = elements;
 Chart.Interaction = Interaction;
 Chart.layouts = layouts;
 Chart.platforms = platforms;
-Chart.platform = platform;
 Chart.plugins = pluginsCore;
 Chart.Scale = Scale;
 Chart.scaleService = scaleService;
diff --git a/src/platform/platform.dom.css b/src/platform/platform.dom.css
deleted file mode 100644 (file)
index 5e74959..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * DOM element rendering detection
- * https://davidwalsh.name/detect-node-insertion
- */
-@keyframes chartjs-render-animation {
-       from { opacity: 0.99; }
-       to { opacity: 1; }
-}
-
-.chartjs-render-monitor {
-       animation: chartjs-render-animation 0.001s;
-}
-
-/*
- * DOM element resizing detection
- * https://github.com/marcj/css-element-queries
- */
-.chartjs-size-monitor,
-.chartjs-size-monitor-expand,
-.chartjs-size-monitor-shrink {
-       position: absolute;
-       direction: ltr;
-       left: 0;
-       top: 0;
-       right: 0;
-       bottom: 0;
-       overflow: hidden;
-       pointer-events: none;
-       visibility: hidden;
-       z-index: -1;
-}
-
-.chartjs-size-monitor-expand > div {
-       position: absolute;
-       width: 1000000px;
-       height: 1000000px;
-       left: 0;
-       top: 0;
-}
-
-.chartjs-size-monitor-shrink > div {
-       position: absolute;
-       width: 200%;
-       height: 200%;
-       left: 0;
-       top: 0;
-}
index 0bc5aebcc8c185365c04960db6e30d6b1f6c71e3..220aa74db61f0a4af95ba510d26f093d81c82b9c 100644 (file)
@@ -4,17 +4,14 @@
 
 import helpers from '../helpers/index';
 import BasePlatform from './platform.base';
-import platform from './platform';
+import {_getParentNode} from '../helpers/helpers.dom';
+import ResizeObserver from 'resize-observer-polyfill';
 
-// @ts-ignore
-import stylesheet from './platform.dom.css';
+/**
+ * @typedef { import("../core/core.controller").default } Chart
+ */
 
 const EXPANDO_KEY = '$chartjs';
-const CSS_PREFIX = 'chartjs-';
-const CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor';
-const CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
-const CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
-const ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];
 
 /**
  * DOM event types -> Chart.js event types.
@@ -52,6 +49,8 @@ function readUsedSize(element, property) {
  * Initializes the canvas style and render size without modifying the canvas display size,
  * since responsiveness is handled by the controller.resize() method. The config is used
  * to determine the aspect ratio to apply in case no explicit height has been specified.
+ * @param {HTMLCanvasElement} canvas
+ * @param {{ options: any; }} config
  */
 function initCanvas(canvas, config) {
        const style = canvas.style;
@@ -78,6 +77,8 @@ function initCanvas(canvas, config) {
        // elements, which would interfere with the responsive resize process.
        // https://github.com/chartjs/Chart.js/issues/2538
        style.display = style.display || 'block';
+       // Include possible borders in the size
+       style.boxSizing = style.boxSizing || 'border-box';
 
        if (renderWidth === null || renderWidth === '') {
                const displayWidth = readUsedSize(canvas, 'width');
@@ -169,147 +170,131 @@ function throttled(fn, thisArg) {
        };
 }
 
-function createDiv(cls) {
-       const el = document.createElement('div');
-       el.className = cls || '';
-       return el;
-}
-
-// Implementation based on https://github.com/marcj/css-element-queries
-function createResizer(domPlatform, handler) {
-       const maxSize = 1000000;
-
-       // NOTE(SB) Don't use innerHTML because it could be considered unsafe.
-       // https://github.com/chartjs/Chart.js/issues/5902
-       const resizer = createDiv(CSS_SIZE_MONITOR);
-       const expand = createDiv(CSS_SIZE_MONITOR + '-expand');
-       const shrink = createDiv(CSS_SIZE_MONITOR + '-shrink');
-
-       expand.appendChild(createDiv());
-       shrink.appendChild(createDiv());
-
-       resizer.appendChild(expand);
-       resizer.appendChild(shrink);
-       domPlatform._reset = function() {
-               expand.scrollLeft = maxSize;
-               expand.scrollTop = maxSize;
-               shrink.scrollLeft = maxSize;
-               shrink.scrollTop = maxSize;
-       };
-
-       const onScroll = function() {
-               domPlatform._reset();
-               handler();
-       };
-
-       addListener(expand, 'scroll', onScroll.bind(expand, 'expand'));
-       addListener(shrink, 'scroll', onScroll.bind(shrink, 'shrink'));
-
-       return resizer;
-}
-
-// https://davidwalsh.name/detect-node-insertion
-function watchForRender(node, handler) {
-       const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
-       const proxy = expando.renderProxy = function(e) {
-               if (e.animationName === CSS_RENDER_ANIMATION) {
-                       handler();
+/**
+ * Watch for resize of `element`.
+ * Calling `fn` is limited to once per animation frame
+ * @param {Element} element - The element to monitor
+ * @param {function} fn - Callback function to call when resized
+ * @return {ResizeObserver}
+ */
+function watchForResize(element, fn) {
+       const resize = throttled((width, height) => {
+               const w = element.clientWidth;
+               fn(width, height);
+               if (w < element.clientWidth) {
+                       // If the container size shrank during chart resize, let's assume
+                       // scrollbar appeared. So we resize again with the scrollbar visible -
+                       // effectively making chart smaller and the scrollbar hidden again.
+                       // Because we are inside `throttled`, and currently `ticking`, scroll
+                       // events are ignored during this whole 2 resize process.
+                       // If we assumed wrong and something else happened, we are resizing
+                       // twice in a frame (potential performance issue)
+                       fn();
                }
-       };
+       }, window);
 
-       ANIMATION_START_EVENTS.forEach((type) => {
-               addListener(node, type, proxy);
+       const observer = new ResizeObserver(entries => {
+               const entry = entries[0];
+               resize(entry.contentRect.width, entry.contentRect.height);
        });
-
-       // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class
-       // is removed then added back immediately (same animation frame?). Accessing the
-       // `offsetParent` property will force a reflow and re-evaluate the CSS animation.
-       // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics
-       // https://github.com/chartjs/Chart.js/issues/4737
-       expando.reflow = !!node.offsetParent;
-
-       node.classList.add(CSS_RENDER_MONITOR);
+       observer.observe(element);
+       return observer;
 }
 
-function unwatchForRender(node) {
-       const expando = node[EXPANDO_KEY] || {};
-       const proxy = expando.renderProxy;
-
-       if (proxy) {
-               ANIMATION_START_EVENTS.forEach((type) => {
-                       removeListener(node, type, proxy);
+/**
+ * Detect attachment of `element` or its direct `parent` to DOM
+ * @param {Element} element - The element to watch for
+ * @param {function} fn - Callback function to call when attachment is detected
+ * @return {MutationObserver}
+ */
+function watchForAttachment(element, fn) {
+       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) {
+                                       fn(entry.target);
+                               }
+                       }
                });
-
-               delete expando.renderProxy;
-       }
-
-       node.classList.remove(CSS_RENDER_MONITOR);
+       });
+       observer.observe(document, {childList: true, subtree: true});
+       return observer;
 }
 
-function addResizeListener(node, listener, chart, domPlatform) {
-       const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
-
-       // Let's keep track of this added resizer and thus avoid DOM query when removing it.
-       const resizer = expando.resizer = createResizer(domPlatform, throttled(() => {
-               if (expando.resizer) {
-                       const container = chart.options.maintainAspectRatio && node.parentNode;
-                       const w = container ? container.clientWidth : 0;
-                       listener(createEvent('resize', chart));
-                       if (container && container.clientWidth < w && chart.canvas) {
-                               // If the container size shrank during chart resize, let's assume
-                               // scrollbar appeared. So we resize again with the scrollbar visible -
-                               // effectively making chart smaller and the scrollbar hidden again.
-                               // Because we are inside `throttled`, and currently `ticking`, scroll
-                               // events are ignored during this whole 2 resize process.
-                               // If we assumed wrong and something else happened, we are resizing
-                               // twice in a frame (potential performance issue)
-                               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, () => {
-               if (expando.resizer) {
-                       const container = node.parentNode;
-                       if (container && container !== resizer.parentNode) {
-                               container.insertBefore(resizer, container.firstChild);
+/**
+ * Watch for detachment of `element` from its direct `parent`.
+ * @param {Element} element - The element to watch
+ * @param {function} fn - Callback function to call when detached.
+ * @return {MutationObserver=}
+ */
+function watchForDetachment(element, fn) {
+       const parent = _getParentNode(element);
+       if (!parent) {
+               return;
+       }
+       const observer = new MutationObserver(entries => {
+               entries.forEach(entry => {
+                       for (let i = 0; i < entry.removedNodes.length; i++) {
+                               if (entry.removedNodes[i] === element) {
+                                       fn();
+                                       break;
+                               }
                        }
-
-                       // The container size might have changed, let's reset the resizer state.
-                       domPlatform._reset();
-               }
+               });
        });
+       observer.observe(parent, {childList: true});
+       return observer;
 }
 
-function removeResizeListener(node) {
-       const expando = node[EXPANDO_KEY] || {};
-       const resizer = expando.resizer;
-
-       delete expando.resizer;
-       unwatchForRender(node);
-
-       if (resizer && resizer.parentNode) {
-               resizer.parentNode.removeChild(resizer);
+/**
+ * @param {{ [x: string]: any; resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
+ * @param {string} type
+ */
+function removeObserver(proxies, type) {
+       const observer = proxies[type];
+       if (observer) {
+               observer.disconnect();
+               proxies[type] = undefined;
        }
 }
 
 /**
- * Injects CSS styles inline if the styles are not already present.
- * @param {Node} rootNode - the HTMLDocument|ShadowRoot node to contain the <style>.
- * @param {string} css - the CSS to be injected.
+ * @param {{ resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
  */
-function injectCSS(rootNode, css) {
-       // https://stackoverflow.com/q/3922139
-       const expando = rootNode[EXPANDO_KEY] || (rootNode[EXPANDO_KEY] = {});
-       if (!expando.containsStyles) {
-               expando.containsStyles = true;
-               css = '/* Chart.js */\n' + css;
-               const style = document.createElement('style');
-               style.setAttribute('type', 'text/css');
-               style.appendChild(document.createTextNode(css));
-               rootNode.appendChild(style);
+function unlistenForResize(proxies) {
+       removeObserver(proxies, 'attach');
+       removeObserver(proxies, 'detach');
+       removeObserver(proxies, 'resize');
+}
+
+/**
+ * @param {HTMLCanvasElement} canvas
+ * @param {{ resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
+ * @param {function} listener
+ */
+function listenForResize(canvas, proxies, listener) {
+       // Helper for recursing when canvas is detached from it's parent
+       const detached = () => listenForResize(canvas, proxies, listener);
+
+       // First make sure all observers are removed
+       unlistenForResize(proxies);
+       // Then check if we are attached
+       const container = _getParentNode(canvas);
+       if (container) {
+               // The canvas is attached (or was immediately re-attached when called through `detached`)
+               proxies.resize = watchForResize(container, listener);
+               proxies.detach = watchForDetachment(canvas, detached);
+       } else {
+               // The canvas is detached
+               proxies.attach = watchForAttachment(canvas, () => {
+                       // The canvas was attached.
+                       removeObserver(proxies, 'attach');
+                       const parent = _getParentNode(canvas);
+                       proxies.resize = watchForResize(parent, listener);
+                       proxies.detach = watchForDetachment(canvas, detached);
+               });
        }
 }
 
@@ -318,39 +303,12 @@ function injectCSS(rootNode, css) {
  * @extends BasePlatform
  */
 export default class DomPlatform extends BasePlatform {
-       /**
-        * @constructor
-        */
-       constructor() {
-               super();
-
-               /**
-                * When `true`, prevents the automatic injection of the stylesheet required to
-                * correctly detect when the chart is added to the DOM and then resized. This
-                * switch has been added to allow external stylesheet (`dist/Chart(.min)?.js`)
-                * to be manually imported to make this library compatible with any CSP.
-                * See https://github.com/chartjs/Chart.js/issues/5208
-                */
-               this.disableCSSInjection = platform.disableCSSInjection;
-       }
 
        /**
-        * Initializes resources that depend on platform options.
-        * @param {HTMLCanvasElement} canvas - The Canvas element.
-        * @private
+        * @param {HTMLCanvasElement} canvas
+        * @param {{ options: { aspectRatio?: number; }; }} config
+        * @return {CanvasRenderingContext2D=}
         */
-       _ensureLoaded(canvas) {
-               if (!this.disableCSSInjection) {
-                       // If the canvas is in a shadow DOM, then the styles must also be inserted
-                       // into the same shadow DOM.
-                       // https://github.com/chartjs/Chart.js/issues/5763
-                       const root = canvas.getRootNode ? canvas.getRootNode() : document;
-                       // @ts-ignore
-                       const targetNode = root.host ? root : document.head;
-                       injectCSS(targetNode, stylesheet);
-               }
-       }
-
        acquireContext(canvas, config) {
                // To prevent canvas fingerprinting, some add-ons undefine the getContext
                // method, for example: https://github.com/kkapsner/CanvasBlocker
@@ -367,7 +325,6 @@ export default class DomPlatform extends BasePlatform {
                if (context && context.canvas === canvas) {
                        // Load platform resources on first chart creation, to make it possible to
                        // import the library before setting platform options.
-                       this._ensureLoaded(canvas);
                        initCanvas(canvas, config);
                        return context;
                }
@@ -375,6 +332,9 @@ export default class DomPlatform extends BasePlatform {
                return null;
        }
 
+       /**
+        * @param {CanvasRenderingContext2D} context
+        */
        releaseContext(context) {
                const canvas = context.canvas;
                if (!canvas[EXPANDO_KEY]) {
@@ -407,39 +367,49 @@ export default class DomPlatform extends BasePlatform {
                return true;
        }
 
+       /**
+        *
+        * @param {Chart} chart
+        * @param {string} type
+        * @param {function} listener
+        */
        addEventListener(chart, type, listener) {
+               // Can have only one listener per type, so make sure previous is removed
+               this.removeEventListener(chart, type);
+
                const canvas = chart.canvas;
+               const proxies = chart.$proxies || (chart.$proxies = {});
                if (type === 'resize') {
-                       // Note: the resize event is not supported on all browsers.
-                       addResizeListener(canvas, listener, chart, this);
-                       return;
+                       return listenForResize(canvas, proxies, listener);
                }
 
-               const expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {});
-               const proxies = expando.proxies || (expando.proxies = {});
-               const proxy = proxies[chart.id + '_' + type] = throttled((event) => {
+               const proxy = proxies[type] = throttled((event) => {
                        listener(fromNativeEvent(event, chart));
                }, chart);
 
                addListener(canvas, type, proxy);
        }
 
-       removeEventListener(chart, type, listener) {
+
+       /**
+        * @param {Chart} chart
+        * @param {string} type
+        */
+       removeEventListener(chart, type) {
                const canvas = chart.canvas;
+               const proxies = chart.$proxies || (chart.$proxies = {});
+
                if (type === 'resize') {
-                       // Note: the resize event is not supported on all browsers.
-                       removeResizeListener(canvas);
-                       return;
+                       return unlistenForResize(proxies);
                }
 
-               const expando = listener[EXPANDO_KEY] || {};
-               const proxies = expando.proxies || {};
-               const proxy = proxies[chart.id + '_' + type];
+               const proxy = proxies[type];
                if (!proxy) {
                        return;
                }
 
                removeListener(canvas, type, proxy);
+               proxies[type] = undefined;
        }
 
        getDevicePixelRatio() {
diff --git a/src/platform/platform.js b/src/platform/platform.js
deleted file mode 100644 (file)
index 739c579..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-
-export default {disableCSSInjection: false};
index 30968a2bff25c8a0ca20757e0fb8d609f8e1d8fa..e234862db6ce4fe2be594f504cd4c8a8bce3d0f8 100644 (file)
@@ -303,20 +303,6 @@ describe('Chart', function() {
                });
        });
 
-       describe('config.options.responsive: false', function() {
-               it('should not inject the resizer element', function() {
-                       var chart = acquireChart({
-                               options: {
-                                       responsive: false
-                               }
-                       });
-
-                       var wrapper = chart.canvas.parentNode;
-                       expect(wrapper.childNodes.length).toBe(1);
-                       expect(wrapper.firstChild.tagName).toBe('CANVAS');
-               });
-       });
-
        describe('config.options.responsive: true (maintainAspectRatio: false)', function() {
                it('should fill parent width and height', function() {
                        var chart = acquireChart({
@@ -633,9 +619,6 @@ describe('Chart', function() {
                        });
 
                        waitForResize(chart, function() {
-                               var resizer = wrapper.firstChild;
-                               expect(resizer.className).toBe('chartjs-size-monitor');
-                               expect(resizer.tagName).toBe('DIV');
                                expect(chart).toBeChartOfSize({
                                        dw: 455, dh: 355,
                                        rw: 455, rh: 355,
@@ -644,8 +627,6 @@ describe('Chart', function() {
                                var target = document.createElement('div');
 
                                waitForResize(chart, function() {
-                                       expect(target.firstChild).toBe(resizer);
-                                       expect(wrapper.firstChild).toBe(null);
                                        expect(chart).toBeChartOfSize({
                                                dw: 640, dh: 480,
                                                rw: 640, rh: 480,
@@ -939,31 +920,6 @@ describe('Chart', function() {
                });
        });
 
-       describe('controller.destroy', function() {
-               it('should remove the resizer element when responsive: true', function(done) {
-                       var chart = acquireChart({
-                               options: {
-                                       responsive: true
-                               }
-                       });
-
-                       waitForResize(chart, function() {
-                               var wrapper = chart.canvas.parentNode;
-                               var resizer = wrapper.firstChild;
-                               expect(wrapper.childNodes.length).toBe(2);
-                               expect(resizer.className).toBe('chartjs-size-monitor');
-                               expect(resizer.tagName).toBe('DIV');
-
-                               chart.destroy();
-
-                               expect(wrapper.childNodes.length).toBe(1);
-                               expect(wrapper.firstChild.tagName).toBe('CANVAS');
-
-                               done();
-                       });
-               });
-       });
-
        describe('controller.reset', function() {
                it('should reset the chart elements', function() {
                        var chart = acquireChart({