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.
* 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;
// 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');
};
}
-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);
+ });
}
}
* @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
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;
}
return null;
}
+ /**
+ * @param {CanvasRenderingContext2D} context
+ */
releaseContext(context) {
const canvas = context.canvas;
if (!canvas[EXPANDO_KEY]) {
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() {