diff --git a/README.md b/README.md index 9ef26a9b9..efe79f426 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,8 @@ To configure the annotations plugin, you can simply add new config options to yo drawTime: 'afterDraw', // (default) // Mouse events to enable on each annotation. - // Similar to the `events` common chart configuration option - // http://www.chartjs.org/docs/#chart-configuration-common-chart-configuration - // Should be an array of: mousemove, mousedown, mouseup, click, dblclick, contextmenu, wheel + // Should be an array of one or more browser-supported mouse events + // See https://developer.mozilla.org/en-US/docs/Web/Events events: ['click'] } } @@ -124,6 +123,12 @@ Vertical or horizontal lines are supported. // Mouse event handlers - be sure to enable the corresponding events in the // annotation events array or the event handler will not be called. + // See https://developer.mozilla.org/en-US/docs/Web/Events for a list of + // supported mouse events. + onMouseenter: function(e) {}, + onMouseover: function(e) {}, + onMouseleave: function(e) {}, + onMouseout: function(e) {}, onMousemove: function(e) {}, onMousedown: function(e) {}, onMouseup: function(e) {}, @@ -172,6 +177,12 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo // Mouse event handlers - be sure to enable the corresponding events in the // annotation events array or the event handler will not be called. + // See https://developer.mozilla.org/en-US/docs/Web/Events for a list of + // supported mouse events. + onMouseenter: function(e) {}, + onMouseover: function(e) {}, + onMouseleave: function(e) {}, + onMouseout: function(e) {}, onMousemove: function(e) {}, onMousedown: function(e) {}, onMouseup: function(e) {}, diff --git a/samples/horizontal-line.html b/samples/horizontal-line.html index db403eddc..3ef248f43 100644 --- a/samples/horizontal-line.html +++ b/samples/horizontal-line.html @@ -127,22 +127,41 @@ }] }, annotation: { - events: ['click'], + events: ['click', 'dblclick', 'mouseover', 'mouseout'], annotations: [{ type: 'line', mode: 'horizontal', scaleID: 'y-axis-1', value: 120, - borderColor: 'black', - borderWidth: 5, + borderColor: 'rgba(255, 0, 0, 0.5)', + borderWidth: 2, onClick: function(e) { - // The annotation is is bound to the `this` variable - alert('Annotation ' + e.type); + this._model.borderColor = 'rgba(255, 0, 0, 1.0)'; + this.chartInstance.render(0); + setTimeout((function() { + this._model.borderColor = 'rgba(255, 0, 0, 0.5)'; + this.chartInstance.render(0); + }).bind(this), 500); + }, + onDblclick: function(e) { + this._model.borderColor = 'rgba(0, 255, 0, 1.0)'; + this.chartInstance.render(0); + setTimeout((function() { + this._model.borderColor = 'rgba(255, 0, 0, 0.5)'; + this.chartInstance.render(0); + }).bind(this), 500); + }, + onMouseover: function(e) { + this._model.borderWidth = 7; + this.chartInstance.render(0); + this.chartInstance.chart.canvas.style.cursor = 'pointer'; + }, + onMouseout: function(e) { + this._model.borderWidth = 5; + this.chartInstance.render(0); + this.chartInstance.chart.canvas.style.cursor = 'initial'; } }] - }, - onClick: function(e, annotation) { - alert('Chart ' + e.type); } } }); diff --git a/src/element.js b/src/element.js index 4da1eab81..2c499019d 100644 --- a/src/element.js +++ b/src/element.js @@ -1,7 +1,11 @@ module.exports = function(Chart) { + var chartHelpers = Chart.helpers; + var AnnotationElement = Chart.Element.extend({ initialize: function() { this.hidden = false; + this.hovering = false; + this._model = chartHelpers.clone(this._model) || {}; this.setDataLimits(); }, destroy: function() {}, diff --git a/src/events.js b/src/events.js new file mode 100644 index 000000000..8be77c1bc --- /dev/null +++ b/src/events.js @@ -0,0 +1,148 @@ +var helpers = require('./helpers.js'); + +module.exports = function(Chart) { + var chartHelpers = Chart.helpers; + + var dblClickSpeed = Chart.Annotation.dblClickSpeed || 350; // ms + + function getEventHandlerName(eventName) { + return 'on' + eventName[0].toUpperCase() + eventName.substring(1); + } + + function createMouseEvent(type, previousEvent) { + try { + return new MouseEvent(type, previousEvent); + } catch (exception) { + try { + var m = document.createEvent('MouseEvent'); + m.initMouseEvent( + type, + previousEvent.canBubble, + previousEvent.cancelable, + previousEvent.view, + previousEvent.detail, + previousEvent.screenX, + previousEvent.screenY, + previousEvent.clientX, + previousEvent.clientY, + previousEvent.ctrlKey, + previousEvent.altKey, + previousEvent.shiftKey, + previousEvent.metaKey, + previousEvent.button, + previousEvent.relatedTarget + ); + return m; + } catch (exception2) { + var e = document.createEvent('Event'); + e.initEvent( + type, + previousEvent.canBubble, + previousEvent.cancelable + ); + return e; + } + } + } + + function collapseHoverEvents(events) { + var hover = false; + var filteredEvents = events.filter(function(eventName) { + switch (eventName) { + case 'mouseenter': + case 'mouseover': + case 'mouseout': + case 'mouseleave': + hover = true; + return false; + + default: + return true; + } + }); + if (hover && filteredEvents.indexOf('mousemove') === -1) { + filteredEvents.push('mousemove'); + } + return filteredEvents; + } + + function dispatcher(e) { + var position = chartHelpers.getRelativePosition(e, this.chart); + var element = helpers.getNearestItems(this.annotations, position); + var events = collapseHoverEvents(this.options.annotation.events); + var eventHandlers = []; + var eventHandlerName = getEventHandlerName(e.type); + var options = (element || {}).options; + + // Detect hover events + if (e.type === 'mousemove') { + if (element && !element.hovering) { + // hover started + ['mouseenter', 'mouseover'].forEach(function(eventName) { + var eventHandlerName = getEventHandlerName(eventName); + var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler + element.hovering = true; + if (typeof options[eventHandlerName] === 'function') { + eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]); + } + }); + } else if (!element) { + // hover ended + this.annotations.forEach(function(element) { + if (element.hovering) { + element.hovering = false; + var options = element.options; + ['mouseout', 'mouseleave'].forEach(function(eventName) { + var eventHandlerName = getEventHandlerName(eventName); + var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler + if (typeof options[eventHandlerName] === 'function') { + eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]); + } + }); + } + }); + } + } + + // Suppress duplicate click events during a double click + // 1. click -> 2. click -> 3. dblclick + // + // 1: wait dblClickSpeed ms, then fire click + // 2: cancel (1) if it is waiting then wait dblClickSpeed ms then fire click, else fire click immediately + // 3: cancel (1) or (2) if waiting, then fire dblclick + if (element && events.indexOf('dblclick') > -1 && typeof options.onDblclick === 'function') { + if (e.type === 'click' && typeof options.onClick === 'function') { + clearTimeout(element.clickTimeout); + element.clickTimeout = setTimeout(function() { + delete element.clickTimeout; + options.onClick.call(element, e); + }, dblClickSpeed); + e.stopImmediatePropagation(); + e.preventDefault(); + return; + } else if (e.type === 'dblclick' && element.clickTimeout) { + clearTimeout(element.clickTimeout); + delete element.clickTimeout; + } + } + + // Dispatch the event to the usual handler, but only if we haven't substituted it + if (element && typeof options[eventHandlerName] === 'function' && eventHandlers.length === 0) { + eventHandlers.push([ options[eventHandlerName], e, element ]); + } + + if (eventHandlers.length > 0) { + e.stopImmediatePropagation(); + e.preventDefault(); + eventHandlers.forEach(function(eventHandler) { + // [handler, event, element] + eventHandler[0].call(eventHandler[2], eventHandler[1]); + }); + } + } + + return { + dispatcher: dispatcher, + collapseHoverEvents: collapseHoverEvents + }; +}; diff --git a/src/helpers.js b/src/helpers.js index 30f2a6d2e..c584d0cee 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -16,10 +16,6 @@ function decorate(obj, prop, func) { } } -function getEventHandlerName(eventName) { - return 'on' + eventName[0].toUpperCase() + eventName.substring(1); -} - function getScaleLimits(scaleId, annotations, scaleMin, scaleMax) { var ranges = annotations.filter(function(annotation) { return !!annotation._model.ranges[scaleId]; @@ -78,7 +74,6 @@ function getNearestItems(annotations, position) { module.exports = { isValid: isValid, decorate: decorate, - getEventHandlerName: getEventHandlerName, getScaleLimits: getScaleLimits, getNearestItems: getNearestItems }; diff --git a/src/plugin.js b/src/plugin.js index 0bef557e6..ba566ad50 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -7,6 +7,10 @@ var helpers = require('./helpers.js'); // Configure plugin namespace Chart.Annotation = Chart.Annotation || {}; +Chart.Annotation.dblClickSpeed = 350; // ms + +var events = require('./events.js')(Chart); + var DRAW_AFTER = 'afterDraw'; var DRAW_AFTER_DATASETS = 'afterDatasetsDraw'; var DRAW_BEFORE_DATASETS = 'beforeDatasetsDraw'; @@ -87,18 +91,6 @@ function build(configs, chartInstance) { }); } -function eventDispatcher(e) { - var position = chartHelpers.getRelativePosition(e, this.chart); - var element = helpers.getNearestItems(this.annotations, position); - var eventHandlerName = helpers.getEventHandlerName(e.type); - var options = (element || {}).options; - if (element && options[eventHandlerName]) { - e.stopImmediatePropagation(); - e.preventDefault(); - options[eventHandlerName].call(element, e); - } -} - var annotationPlugin = { beforeInit: function(chartInstance) { chartInstance.annotations = []; @@ -131,20 +123,20 @@ var annotationPlugin = { // Detect and intercept events that happen on an annotation element var config = chartInstance.options.annotation || {}; - if (config.events) { - chartInstance._annotationEventHandler = eventDispatcher.bind(chartInstance); - config.events.forEach(function(eventName) { + var watchFor = config.events || []; + if (watchFor) { + chartInstance._annotationEventHandler = events.dispatcher.bind(chartInstance); + events.collapseHoverEvents(watchFor).forEach(function(eventName) { chartHelpers.addEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler); }); } }, destroy: function(chartInstance) { var config = chartInstance.annotations._config; - if (config.events.length > 0) { - config.events.forEach(function(eventName) { - chartHelpers.removeEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler); - }); - } + var events = config.events || []; + events.forEach(function(eventName) { + chartHelpers.removeEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler); + }); }, beforeUpdate: function(chartInstance) { // Build the configuration with all the defaults set diff --git a/src/types/box.js b/src/types/box.js index 44542139f..024d9412d 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -4,7 +4,7 @@ var helpers = require('../helpers.js'); module.exports = function(Chart) { var BoxAnnotation = Chart.Annotation.Element.extend({ setDataLimits: function() { - var model = this._model = this._model || {}; + var model = this._model; var options = this.options; var chartInstance = this.chartInstance; @@ -36,7 +36,7 @@ module.exports = function(Chart) { } }, configure: function() { - var model = this._model = this._model || {}; + var model = this._model; var options = this.options; var chartInstance = this.chartInstance; @@ -85,23 +85,27 @@ module.exports = function(Chart) { model.backgroundColor = options.backgroundColor; }, inRange: function(mouseX, mouseY) { - return this._view && - mouseX >= this._view.left && - mouseX <= this._view.right && - mouseY >= this._view.top && - mouseY <= this._view.bottom; + var model = this._model; + return model && + mouseX >= model.left && + mouseX <= model.right && + mouseY >= model.top && + mouseY <= model.bottom; }, getCenterPoint: function() { + var model = this._model; return { - x: (this._view.right + this._view.left) / 2, - y: (this._view.bottom + this._view.top) / 2 + x: (model.right + model.left) / 2, + y: (model.bottom + model.top) / 2 }; }, getWidth: function() { - return Math.abs(this._view.right - this._view.left); + var model = this._model; + return Math.abs(model.right - model.left); }, getHeight: function() { - return Math.abs(this._view.bottom - this._view.top); + var model = this._model; + return Math.abs(model.bottom - model.top); }, getArea: function() { return this.getWidth() * this.getHeight(); diff --git a/src/types/line.js b/src/types/line.js index 21056d684..d34aaa72b 100644 --- a/src/types/line.js +++ b/src/types/line.js @@ -10,7 +10,7 @@ module.exports = function(Chart) { var LineAnnotation = Chart.Annotation.Element.extend({ setDataLimits: function() { - var model = this._model = chartHelpers.clone(this._model) || {}; + var model = this._model; var options = this.options; // Set the data range for this annotation @@ -21,7 +21,7 @@ module.exports = function(Chart) { }; }, configure: function() { - var model = this._model = chartHelpers.clone(this._model) || {}; + var model = this._model; var options = this.options; var chartInstance = this.chartInstance; var ctx = this.ctx; @@ -92,20 +92,20 @@ module.exports = function(Chart) { model.borderDashOffset = options.borderDashOffset || 0; }, inRange: function(mouseX, mouseY) { - var model = this._model || {}; + var model = this._model; return (model.line && model.line.intersects(mouseX, mouseY, this.getHeight())); }, getCenterPoint: function() { return { - x: (this._view.x2 + this._view.x1) / 2, - y: (this._view.y2 + this._view.y1) / 2 + x: (this._model.x2 + this._model.x1) / 2, + y: (this._model.y2 + this._model.y1) / 2 }; }, getWidth: function() { - return Math.abs(this._view.right - this._view.left); + return Math.abs(this._model.right - this._model.left); }, getHeight: function() { - return this._view.borderWidth || 1; + return this._model.borderWidth || 1; }, getArea: function() { return Math.sqrt(Math.pow(this.getWidth(), 2) + Math.pow(this.getHeight(), 2));