diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index c1ce806003e..5f1decb3177 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -63,6 +63,7 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `scales.[x/y]Axes.time.max` was renamed to `scales[id].max` * `scales.[x/y]Axes.time.min` was renamed to `scales[id].min` * The dataset option `tension` was renamed to `lineTension` +* To override the platform class used in a chart instance, pass `platform: PlatformClass` in the config object. Note that the class should be passed, not an instance of the class. ### Animations @@ -162,6 +163,7 @@ Animation system was completely rewritten in Chart.js v3. Each property can now * `helpers._alignPixel` was renamed to `helpers.canvas._alignPixel` * `helpers._decimalPlaces` was renamed to `helpers.math._decimalPlaces` +* `chart.initialize` was renamed to `chart._initialize` (labeled as private but not named as such) ### Changed @@ -207,3 +209,8 @@ Animation system was completely rewritten in Chart.js v3. Each property can now * The second parameter to `drawPoint` is now the full options object, so `style`, `rotation`, and `radius` are no longer passed explicitly +#### 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.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. diff --git a/samples/advanced/content-security-policy.js b/samples/advanced/content-security-policy.js index a185332f74b..a974fc6f349 100644 --- a/samples/advanced/content-security-policy.js +++ b/samples/advanced/content-security-policy.js @@ -1,10 +1,9 @@ var utils = Samples.utils; +utils.srand(110); // CSP: disable automatic style injection Chart.platform.disableCSSInjection = true; -utils.srand(110); - function generateData() { var DATA_COUNT = 16; var MIN_XY = -150; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 3d4ca427c38..2da5705ebf4 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -6,7 +6,7 @@ import defaults from './core.defaults'; import helpers from '../helpers/index'; import Interaction from './core.interaction'; import layouts from './core.layouts'; -import platform from '../platforms/platform'; +import {BasicPlatform, DomPlatform} from '../platform/platforms'; import plugins from './core.plugins'; import scaleService from '../core/core.scaleService'; @@ -145,13 +145,38 @@ function onAnimationProgress(ctx) { helpers.callback(animationOptions && animationOptions.onProgress, arguments, chart); } +function isDomSupported() { + return typeof window !== undefined && typeof document !== undefined; +} + +/** + * Chart.js can take a string id of a canvas element, a 2d context, or a canvas element itself. + * Attempt to unwrap the item passed into the chart constructor so that it is a canvas element (if possible). + */ +function getCanvas(item) { + if (isDomSupported() && typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + return item; +} + class Chart { constructor(item, config) { const me = this; config = initConfig(config); + const initialCanvas = getCanvas(item); + me._initializePlatform(initialCanvas, config); - const context = platform.acquireContext(item, config); + const context = me.platform.acquireContext(initialCanvas, config); const canvas = context && context.canvas; const height = canvas && canvas.height; const width = canvas && canvas.width; @@ -193,14 +218,14 @@ class Chart { Animator.listen(me, 'complete', onAnimationsComplete); Animator.listen(me, 'progress', onAnimationProgress); - me.initialize(); + me._initialize(); me.update(); } /** * @private */ - initialize() { + _initialize() { const me = this; // Before init plugin notification @@ -221,6 +246,23 @@ class Chart { return me; } + /** + * @private + */ + _initializePlatform(canvas, config) { + const me = this; + + if (config.platform) { + me.platform = new config.platform(); + } else if (!isDomSupported()) { + me.platform = new BasicPlatform(); + } else if (window.OffscreenCanvas && canvas instanceof window.OffscreenCanvas) { + me.platform = new BasicPlatform(); + } else { + me.platform = new DomPlatform(); + } + } + clear() { helpers.canvas.clear(this); return this; @@ -244,7 +286,7 @@ class Chart { // 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 newRatio = options.devicePixelRatio || platform.getDevicePixelRatio(); + const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio(); if (me.width === newWidth && me.height === newHeight && oldRatio === newRatio) { return; @@ -824,7 +866,7 @@ class Chart { if (canvas) { me.unbindEvents(); helpers.canvas.clear(me); - platform.releaseContext(me.ctx); + me.platform.releaseContext(me.ctx); me.canvas = null; me.ctx = null; } @@ -849,7 +891,7 @@ class Chart { }; helpers.each(me.options.events, function(type) { - platform.addEventListener(me, type, listener); + me.platform.addEventListener(me, type, listener); listeners[type] = listener; }); @@ -860,7 +902,7 @@ class Chart { me.resize(); }; - platform.addEventListener(me, 'resize', listener); + me.platform.addEventListener(me, 'resize', listener); listeners.resize = listener; } } @@ -877,7 +919,7 @@ class Chart { delete me._listeners; helpers.each(listeners, function(listener, type) { - platform.removeEventListener(me, type, listener); + me.platform.removeEventListener(me, type, listener); }); } diff --git a/src/index.js b/src/index.js index e472b1528fa..b32770a0972 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,8 @@ import Element from './core/core.element'; import elements from './elements'; import Interaction from './core/core.interaction'; import layouts from './core/core.layouts'; -import platform from './platforms/platform'; +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'; @@ -33,6 +34,7 @@ Chart.Element = Element; Chart.elements = elements; Chart.Interaction = Interaction; Chart.layouts = layouts; +Chart.platforms = platforms; Chart.platform = platform; Chart.plugins = pluginsCore; Chart.Scale = Scale; @@ -57,8 +59,6 @@ for (var k in plugins) { } } -Chart.platform.initialize(); - if (typeof window !== 'undefined') { window.Chart = Chart; } diff --git a/src/platforms/platform.js b/src/platform/platform.base.js similarity index 61% rename from src/platforms/platform.js rename to src/platform/platform.base.js index a135c099263..5ef81f74498 100644 --- a/src/platforms/platform.js +++ b/src/platform/platform.base.js @@ -1,31 +1,22 @@ 'use strict'; -import helpers from '../helpers/index'; -import basic from './platform.basic'; -import dom from './platform.dom'; - -// @TODO Make possible to select another platform at build time. -const implementation = dom._enabled ? dom : basic; - /** - * @namespace Chart.platform - * @see https://chartjs.gitbooks.io/proposals/content/Platform.html - * @since 2.4.0 + * Abstract class that allows abstracting platform dependencies away from the chart. */ -export default helpers.extend({ +export default class BasePlatform { /** - * @since 2.7.0 + * @constructor */ - initialize: function() {}, + constructor() {} /** * Called at chart construction time, returns a context2d instance implementing * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. - * @param {*} item - The native item from which to acquire context (platform specific) + * @param {canvas} canvas - The canvas from which to acquire context (platform specific) * @param {object} options - The chart options * @returns {CanvasRenderingContext2D} context2d instance */ - acquireContext: function() {}, + acquireContext() {} /** * Called at chart destruction time, releases any resources associated to the context @@ -33,7 +24,7 @@ export default helpers.extend({ * @param {CanvasRenderingContext2D} context - The context2d instance * @returns {boolean} true if the method succeeded, else false */ - releaseContext: function() {}, + releaseContext() {} /** * Registers the specified listener on the given chart. @@ -42,7 +33,7 @@ export default helpers.extend({ * @param {function} listener - Receives a notification (an object that implements * the {@link IEvent} interface) when an event of the specified type occurs. */ - addEventListener: function() {}, + addEventListener() {} /** * Removes the specified listener previously registered with addEventListener. @@ -50,25 +41,15 @@ export default helpers.extend({ * @param {string} type - The ({@link IEvent}) type to remove * @param {function} listener - The listener function to remove from the event target. */ - removeEventListener: function() {}, + removeEventListener() {} /** - * Returs current devicePixelRatio of the device this platform is connected to. + * @returns {number} the current devicePixelRatio of the device this platform is connected to. */ - getDevicePixelRatio: function() { + getDevicePixelRatio() { return 1; } - -}, implementation); - -/** - * @interface IPlatform - * Allows abstracting platform dependencies away from the chart - * @borrows Chart.platform.acquireContext as acquireContext - * @borrows Chart.platform.releaseContext as releaseContext - * @borrows Chart.platform.addEventListener as addEventListener - * @borrows Chart.platform.removeEventListener as removeEventListener - */ +} /** * @interface IEvent diff --git a/src/platform/platform.basic.js b/src/platform/platform.basic.js new file mode 100644 index 00000000000..2f2d52c3bc3 --- /dev/null +++ b/src/platform/platform.basic.js @@ -0,0 +1,22 @@ +/** + * Platform fallback implementation (minimal). + * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 + */ + +'use strict'; + +import BasePlatform from './platform.base'; + +/** + * Platform class for charts without access to the DOM or to many element properties + * This platform is used by default for any chart passed an OffscreenCanvas. + * @extends BasePlatform + */ +export default class BasicPlatform extends BasePlatform { + acquireContext(item) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + return item && item.getContext && item.getContext('2d') || null; + } +} diff --git a/src/platforms/platform.dom.css b/src/platform/platform.dom.css similarity index 100% rename from src/platforms/platform.dom.css rename to src/platform/platform.dom.css diff --git a/src/platforms/platform.dom.js b/src/platform/platform.dom.js similarity index 89% rename from src/platforms/platform.dom.js rename to src/platform/platform.dom.js index 6a7c07a2226..4b1cd2b7d59 100644 --- a/src/platforms/platform.dom.js +++ b/src/platform/platform.dom.js @@ -6,6 +6,8 @@ import helpers from '../helpers'; import stylesheet from './platform.dom.css'; +import BasePlatform from './platform.base'; +import platform from './platform'; var EXPANDO_KEY = '$chartjs'; var CSS_PREFIX = 'chartjs-'; @@ -312,29 +314,33 @@ function injectCSS(rootNode, css) { } } -export default { +/** + * Platform class for charts that can access the DOM and global window/document properties + * @extends BasePlatform + */ +export default class DomPlatform extends BasePlatform { /** - * 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 + * @constructor */ - disableCSSInjection: false, - - /** - * This property holds whether this platform is enabled for the current environment. - * Currently used by platform.js to select the proper implementation. - * @private - */ - _enabled: typeof window !== 'undefined' && typeof document !== 'undefined', + 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 */ - _ensureLoaded: function(canvas) { + _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. @@ -343,45 +349,33 @@ export default { var targetNode = root.host ? root : document.head; injectCSS(targetNode, stylesheet); } - }, - - acquireContext: function(item, config) { - if (typeof item === 'string') { - item = document.getElementById(item); - } else if (item.length) { - // Support for array based queries (such as jQuery) - item = item[0]; - } - - if (item && item.canvas) { - // Support for any object associated to a canvas (including a context2d) - item = item.canvas; - } + } + acquireContext(canvas, config) { // To prevent canvas fingerprinting, some add-ons undefine the getContext // method, for example: https://github.com/kkapsner/CanvasBlocker // https://github.com/chartjs/Chart.js/issues/2807 - var context = item && item.getContext && item.getContext('2d'); + var context = canvas && canvas.getContext && canvas.getContext('2d'); - // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is + // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the canvas is // inside an iframe or when running in a protected environment. We could guess the // types from their toString() value but let's keep things flexible and assume it's - // a sufficient condition if the item has a context2D which has item as `canvas`. + // a sufficient condition if the canvas has a context2D which has canvas as `canvas`. // https://github.com/chartjs/Chart.js/issues/3887 // https://github.com/chartjs/Chart.js/issues/4102 // https://github.com/chartjs/Chart.js/issues/4152 - if (context && context.canvas === item) { + 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(item); - initCanvas(item, config); + this._ensureLoaded(canvas); + initCanvas(canvas, config); return context; } return null; - }, + } - releaseContext: function(context) { + releaseContext(context) { const canvas = context.canvas; if (!canvas[EXPANDO_KEY]) { return; @@ -410,9 +404,9 @@ export default { canvas.width = canvas.width; delete canvas[EXPANDO_KEY]; - }, + } - addEventListener: function(chart, type, listener) { + addEventListener(chart, type, listener) { var canvas = chart.canvas; if (type === 'resize') { // Note: the resize event is not supported on all browsers. @@ -427,9 +421,9 @@ export default { }, chart); addListener(canvas, type, proxy); - }, + } - removeEventListener: function(chart, type, listener) { + removeEventListener(chart, type, listener) { var canvas = chart.canvas; if (type === 'resize') { // Note: the resize event is not supported on all browsers. @@ -445,9 +439,9 @@ export default { } removeListener(canvas, type, proxy); - }, + } - getDevicePixelRatio: function() { + getDevicePixelRatio() { return window.devicePixelRatio; } -}; +} diff --git a/src/platform/platform.js b/src/platform/platform.js new file mode 100644 index 00000000000..ec81caef8b5 --- /dev/null +++ b/src/platform/platform.js @@ -0,0 +1,3 @@ +'use strict'; + +export default {disableCSSInjection: false}; diff --git a/src/platform/platforms.js b/src/platform/platforms.js new file mode 100644 index 00000000000..a8701f21354 --- /dev/null +++ b/src/platform/platforms.js @@ -0,0 +1,13 @@ +'use strict'; + +import BasePlatform from './platform.base'; +import BasicPlatform from './platform.basic'; +import DomPlatform from './platform.dom'; + +export {BasicPlatform, DomPlatform, BasePlatform}; + +/** + * @namespace Chart.platforms + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html +*/ +export default {BasicPlatform, DomPlatform, BasePlatform}; diff --git a/src/platforms/platform.basic.js b/src/platforms/platform.basic.js deleted file mode 100644 index f510b3575c7..00000000000 --- a/src/platforms/platform.basic.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Platform fallback implementation (minimal). - * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 - */ - -module.exports = { - acquireContext: function(item) { - if (item && item.canvas) { - // Support for any object associated to a canvas (including a context2d) - item = item.canvas; - } - - return item && item.getContext('2d') || null; - } -}; diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js index 8e4ffa5e7e7..975670c3a41 100644 --- a/test/specs/global.namespace.tests.js +++ b/test/specs/global.namespace.tests.js @@ -12,7 +12,7 @@ describe('Chart namespace', function() { expect(Chart.Interaction instanceof Object).toBeTruthy(); expect(Chart.layouts instanceof Object).toBeTruthy(); expect(Chart.plugins instanceof Object).toBeTruthy(); - expect(Chart.platform instanceof Object).toBeTruthy(); + expect(Chart.platforms instanceof Object).toBeTruthy(); expect(Chart.Scale instanceof Object).toBeTruthy(); expect(Chart.scaleService instanceof Object).toBeTruthy(); expect(Chart.Ticks instanceof Object).toBeTruthy();