mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
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:
parent
7397a41fac
commit
bfe34214ac
@ -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;
|
||||
```
|
||||
|
||||
@ -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
23
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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'
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
|
||||
export default {disableCSSInjection: false};
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user