Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for detached canvas element #4591

Merged
merged 2 commits into from Aug 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/chart.js
Expand Up @@ -60,6 +60,8 @@ plugins.push(

Chart.plugins.register(plugins);

Chart.platform.initialize();

module.exports = Chart;
if (typeof window !== 'undefined') {
window.Chart = Chart;
Expand Down
8 changes: 8 additions & 0 deletions src/core/core.helpers.js
Expand Up @@ -500,6 +500,10 @@ module.exports = function(Chart) {
};
helpers.getMaximumWidth = function(domNode) {
var container = domNode.parentNode;
if (!container) {
return domNode.clientWidth;
}

var paddingLeft = parseInt(helpers.getStyle(container, 'padding-left'), 10);
var paddingRight = parseInt(helpers.getStyle(container, 'padding-right'), 10);
var w = container.clientWidth - paddingLeft - paddingRight;
Expand All @@ -508,6 +512,10 @@ module.exports = function(Chart) {
};
helpers.getMaximumHeight = function(domNode) {
var container = domNode.parentNode;
if (!container) {
return domNode.clientHeight;
}

var paddingTop = parseInt(helpers.getStyle(container, 'padding-top'), 10);
var paddingBottom = parseInt(helpers.getStyle(container, 'padding-bottom'), 10);
var h = container.clientHeight - paddingTop - paddingBottom;
Expand Down
158 changes: 116 additions & 42 deletions src/platforms/platform.dom.js
Expand Up @@ -6,19 +6,21 @@

var helpers = require('../helpers/index');

var EXPANDO_KEY = '$chartjs';
var CSS_PREFIX = 'chartjs-';
var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];

/**
* DOM event types -> Chart.js event types.
* Note: only events with different types are mapped.
* @see https://developer.mozilla.org/en-US/docs/Web/Events
*/

var eventTypeMap = {
// Touch events
var EVENT_TYPES = {
touchstart: 'mousedown',
touchmove: 'mousemove',
touchend: 'mouseup',

// Pointer events
pointerenter: 'mouseenter',
pointerdown: 'mousedown',
pointermove: 'mousemove',
Expand Down Expand Up @@ -56,7 +58,7 @@ function initCanvas(canvas, config) {
var renderWidth = canvas.getAttribute('width');

// Chart.js modifies some canvas values that we want to restore on destroy
canvas._chartjs = {
canvas[EXPANDO_KEY] = {
initial: {
height: renderHeight,
width: renderWidth,
Expand Down Expand Up @@ -140,11 +142,29 @@ function createEvent(type, chart, x, y, nativeEvent) {
}

function fromNativeEvent(event, chart) {
var type = eventTypeMap[event.type] || event.type;
var type = EVENT_TYPES[event.type] || event.type;
var pos = helpers.getRelativePosition(event, chart);
return createEvent(type, chart, pos.x, pos.y, event);
}

function throttled(fn, thisArg) {
var ticking = false;
var args = [];

return function() {
args = Array.prototype.slice.call(arguments);
thisArg = thisArg || this;

if (!ticking) {
ticking = true;
helpers.requestAnimFrame.call(window, function() {
ticking = false;
fn.apply(thisArg, args);
});
}
};
}

function createResizer(handler) {
var iframe = document.createElement('iframe');
iframe.className = 'chartjs-hidden-iframe';
Expand Down Expand Up @@ -176,7 +196,6 @@ function createResizer(handler) {
// https://github.com/chartjs/Chart.js/issues/3521
addEventListener(iframe, 'load', function() {
addEventListener(iframe.contentWindow || iframe, 'resize', handler);

// The iframe size might have changed while loading, which can also
// happen if the size has been changed while detached from the DOM.
handler();
Expand All @@ -185,45 +204,100 @@ function createResizer(handler) {
return iframe;
}

function addResizeListener(node, listener, chart) {
var stub = node._chartjs = {
ticking: false
};

// Throttle the callback notification until the next animation frame.
var notify = function() {
if (!stub.ticking) {
stub.ticking = true;
helpers.requestAnimFrame.call(window, function() {
if (stub.resizer) {
stub.ticking = false;
return listener(createEvent('resize', chart));
}
});
// https://davidwalsh.name/detect-node-insertion
function watchForRender(node, handler) {
var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
var proxy = expando.renderProxy = function(e) {
if (e.animationName === CSS_RENDER_ANIMATION) {
handler();
}
};

// Let's keep track of this added iframe and thus avoid DOM query when removing it.
stub.resizer = createResizer(notify);
helpers.each(ANIMATION_START_EVENTS, function(type) {
addEventListener(node, type, proxy);
});

node.insertBefore(stub.resizer, node.firstChild);
node.classList.add(CSS_RENDER_MONITOR);
}

function removeResizeListener(node) {
if (!node || !node._chartjs) {
return;
function unwatchForRender(node) {
var expando = node[EXPANDO_KEY] || {};
var proxy = expando.renderProxy;

if (proxy) {
helpers.each(ANIMATION_START_EVENTS, function(type) {
removeEventListener(node, type, proxy);
});

delete expando.renderProxy;
}

var resizer = node._chartjs.resizer;
if (resizer) {
node.classList.remove(CSS_RENDER_MONITOR);
}

function addResizeListener(node, listener, chart) {
var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});

// Let's keep track of this added resizer and thus avoid DOM query when removing it.
var resizer = expando.resizer = createResizer(throttled(function() {
if (expando.resizer) {
return 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, function() {
if (expando.resizer) {
var container = node.parentNode;
if (container && container !== resizer.parentNode) {
container.insertBefore(resizer, container.firstChild);
}
}
});
}

function removeResizeListener(node) {
var expando = node[EXPANDO_KEY] || {};
var resizer = expando.resizer;

delete expando.resizer;
unwatchForRender(node);

if (resizer && resizer.parentNode) {
resizer.parentNode.removeChild(resizer);
node._chartjs.resizer = null;
}
}

function injectCSS(platform, css) {
// http://stackoverflow.com/q/3922139
var style = platform._style || document.createElement('style');
if (!platform._style) {
platform._style = style;
css = '/* Chart.js */\n' + css;
style.setAttribute('type', 'text/css');
document.getElementsByTagName('head')[0].appendChild(style);
}

delete node._chartjs;
style.appendChild(document.createTextNode(css));
}

module.exports = {
initialize: function() {
var keyframes = 'from{opacity:0.99}to{opacity:1}';

injectCSS(this,
// DOM rendering detection
// https://davidwalsh.name/detect-node-insertion
'@-webkit-keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
'@keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
'.' + CSS_RENDER_MONITOR + '{' +
'-webkit-animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
'animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
'}'
);
},

acquireContext: function(item, config) {
if (typeof item === 'string') {
item = document.getElementById(item);
Expand Down Expand Up @@ -259,11 +333,11 @@ module.exports = {

releaseContext: function(context) {
var canvas = context.canvas;
if (!canvas._chartjs) {
if (!canvas[EXPANDO_KEY]) {
return;
}

var initial = canvas._chartjs.initial;
var initial = canvas[EXPANDO_KEY].initial;
['height', 'width'].forEach(function(prop) {
var value = initial[prop];
if (helpers.isNullOrUndef(value)) {
Expand All @@ -283,19 +357,19 @@ module.exports = {
// https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html
canvas.width = canvas.width;

delete canvas._chartjs;
delete canvas[EXPANDO_KEY];
},

addEventListener: function(chart, type, listener) {
var canvas = chart.canvas;
if (type === 'resize') {
// Note: the resize event is not supported on all browsers.
addResizeListener(canvas.parentNode, listener, chart);
addResizeListener(canvas, listener, chart);
return;
}

var stub = listener._chartjs || (listener._chartjs = {});
var proxies = stub.proxies || (stub.proxies = {});
var expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {});
var proxies = expando.proxies || (expando.proxies = {});
var proxy = proxies[chart.id + '_' + type] = function(event) {
listener(fromNativeEvent(event, chart));
};
Expand All @@ -307,12 +381,12 @@ module.exports = {
var canvas = chart.canvas;
if (type === 'resize') {
// Note: the resize event is not supported on all browsers.
removeResizeListener(canvas.parentNode, listener);
removeResizeListener(canvas, listener);
return;
}

var stub = listener._chartjs || {};
var proxies = stub.proxies || {};
var expando = listener[EXPANDO_KEY] || {};
var proxies = expando.proxies || {};
var proxy = proxies[chart.id + '_' + type];
if (!proxy) {
return;
Expand Down
5 changes: 5 additions & 0 deletions src/platforms/platform.js
Expand Up @@ -12,6 +12,11 @@ var implementation = require('./platform.dom');
* @since 2.4.0
*/
module.exports = helpers.extend({
/**
* @since 2.7.0
*/
initialize: function() {},

/**
* Called at chart construction time, returns a context2d instance implementing
* the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.
Expand Down
4 changes: 2 additions & 2 deletions test/jasmine.matchers.js
Expand Up @@ -123,8 +123,8 @@ function toBeChartOfSize() {
var canvas = actual.ctx.canvas;
var style = getComputedStyle(canvas);
var pixelRatio = actual.options.devicePixelRatio || window.devicePixelRatio;
var dh = parseInt(style.height, 10);
var dw = parseInt(style.width, 10);
var dh = parseInt(style.height, 10) || 0;
var dw = parseInt(style.width, 10) || 0;
var rh = canvas.height;
var rw = canvas.width;
var orh = rh / pixelRatio;
Expand Down