Use ResizeObserver and MutationObserver to detect detach/attach/resize (#7104)

* Use Resize/MutationObserver to detect detach/attach/resize
* Cleanup
* Review update
* Restore infinite resize detection (#6011)
This commit is contained in:
Jukka Kurkela 2020-02-17 18:00:03 +02:00 committed by GitHub
parent 7397a41fac
commit bfe34214ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 189 additions and 494 deletions

View 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;
```

View 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.

23
package-lock.json generated
View File

@ -3640,23 +3640,6 @@
}
}
},
"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",
@ -14463,6 +14446,12 @@
"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",

View 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",

View 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

View 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
};

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}
}
}
});
});

View File

@ -244,9 +244,6 @@
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'

View 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);

View 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;

View 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;

View File

@ -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;
}

View 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);
}
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);
}
// The container size might have changed, let's reset the resizer state.
domPlatform._reset();
}
});
observer.observe(document, {childList: true, subtree: true});
return observer;
}
function removeResizeListener(node) {
const expando = node[EXPANDO_KEY] || {};
const resizer = expando.resizer;
/**
* 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;
}
}
});
});
observer.observe(parent, {childList: true});
return observer;
}
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() {

View File

@ -1,2 +0,0 @@
export default {disableCSSInjection: false};

View 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({