diff --git a/HISTORY.md b/HISTORY.md index 43dcd736..8fe0b926 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -16,6 +16,7 @@ http://visjs.org - Fixed accidental redrawing during stabilization. - Fixed delete callbacks with null argument not showing toolbar afterwards. - Added zoom events from keyboard and navigation buttons. +- No longer start stabilization with an empty node set. ## 2015-06-16, version 4.3.0 diff --git a/dist/vis.js b/dist/vis.js index 77f634f7..dba99610 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -2084,6 +2084,10 @@ return /******/ (function(modules) { // webpackBootstrap iterations = this.options.stabilization.iterations; } + if (this.physicsBody.physicsNodeIndices.length === 0) { + return; + } + // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit('_resizeNodes'); @@ -2680,3991 +2684,3991 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(2); var Hammer = __webpack_require__(31); var hammerUtil = __webpack_require__(34); - var keycharm = __webpack_require__(49); - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { + /** + * clears the toolbar div element of children + * + * @private + */ + + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { var _this = this; - _classCallCheck(this, NavigationHandler); + _classCallCheck(this, ManipulationSystem); this.body = body; this.canvas = canvas; + this.selectionHandler = selectionHandler; + + this.editMode = false; + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + + this.manipulationHammers = []; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; this.touchTime = 0; - this.activated = false; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.inMode = false; + this.selectedControlNode = undefined; - this.body.emitter.on('release', function () { - _this._stopMovement(); - }); - this.body.emitter.on('activate', function () { - _this.activated = true;_this.configureKeyboardBindings(); - }); - this.body.emitter.on('deactivate', function () { - _this.activated = false;_this.configureKeyboardBindings(); - }); - this.body.emitter.on('destroy', function () { - if (_this.keycharm !== undefined) { - _this.keycharm.destroy(); + this.options = {}; + this.defaultOptions = { + enabled: false, + initiallyActive: false, + addNode: true, + addEdge: true, + editNode: undefined, + editEdge: true, + deleteNode: true, + deleteEdge: true, + controlNodeStyle: { + shape: 'dot', + size: 6, + color: { background: '#ff0000', border: '#3c3c3c', highlight: { background: '#07f968', border: '#3c3c3c' } }, + borderWidth: 2, + borderWidthSelected: 2 } - }); + }; + util.extend(this.options, this.defaultOptions); - this.options = {}; + this.body.emitter.on('destroy', function () { + _this._clean(); + }); + this.body.emitter.on('_dataChanged', this._restore.bind(this)); + this.body.emitter.on('_resetData', this._restore.bind(this)); } - _createClass(NavigationHandler, [{ - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); + _createClass(ManipulationSystem, [{ + key: '_restore', + + /** + * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. + * @private + */ + value: function _restore() { + if (this.inMode !== false) { + if (this.options.initiallyActive === true) { + this.enableEditMode(); + } else { + this.disableEditMode(); + } } } }, { - key: 'create', - value: function create() { - if (this.options.navigationButtons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); + key: 'setOptions', + + /** + * Set the Options + * @param options + */ + value: function setOptions(options, allOptions, globalOptions) { + if (allOptions !== undefined) { + if (allOptions.locale !== undefined) { + this.options.locale = allOptions.locale; + } else { + this.options.locale = globalOptions.locale; + } + if (allOptions.locales !== undefined) { + this.options.locales = allOptions.locales; + } else { + this.options.locales = globalOptions.locales; } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); } - this.configureKeyboardBindings(); + if (options !== undefined) { + if (typeof options === 'boolean') { + this.options.enabled = options; + } else { + this.options.enabled = true; + util.deepExtend(this.options, options); + } + if (this.options.initiallyActive === true) { + this.editMode = true; + } + this._setup(); + } } }, { - key: 'cleanNavigation', - value: function cleanNavigation() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].destroy(); - } - this.navigationHammers = []; + key: 'toggleEditMode', + + /** + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * + * @private + */ + value: function toggleEditMode() { + if (this.editMode === true) { + this.disableEditMode(); + } else { + this.enableEditMode(); } + } + }, { + key: 'enableEditMode', + value: function enableEditMode() { + this.editMode = true; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { - this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; + this.editModeDiv.style.display = 'none'; + this.showManipulatorToolbar(); } + } + }, { + key: 'disableEditMode', + value: function disableEditMode() { + this.editMode = false; - this.iconsCreated = false; + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'none'; + this.closeDiv.style.display = 'none'; + this.editModeDiv.style.display = 'block'; + this._createEditButton(); + } } }, { - key: 'loadNavigationElements', + key: 'showManipulatorToolbar', /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * * @private */ - value: function loadNavigationElements() { - this.cleanNavigation(); + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - this.navigationDOM = {}; - var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; - var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; + // reset global letiables + this.manipulationDOM = {}; - this.navigationDOM['wrapper'] = document.createElement('div'); - this.navigationDOM['wrapper'].className = 'vis-navigation'; - this.canvas.frame.appendChild(this.navigationDOM['wrapper']); + // if the gui is enabled, draw all elements. + if (this.guiEnabled === true) { + // a _restore will hide these menus + this.editMode = true; + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement('div'); - this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i]; - this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); + var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var selectedTotalCount = selectedNodeCount + selectedEdgeCount; + var locale = this.options.locales[this.options.locale]; + var needSeperator = false; - var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); - if (navigationDivActions[i] === '_fit') { - hammerUtil.onTouch(hammer, this._fit.bind(this)); - } else { - hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); + if (this.options.addNode !== false) { + this._createAddNodeButton(locale); + needSeperator = true; + } + if (this.options.addEdge !== false) { + if (needSeperator === true) { + this._createSeperator(1); + } else { + needSeperator = true; + } + this._createAddEdgeButton(locale); } - this.navigationHammers.push(hammer); + if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { + if (needSeperator === true) { + this._createSeperator(2); + } else { + needSeperator = true; + } + this._createEditNodeButton(locale); + } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { + if (needSeperator === true) { + this._createSeperator(3); + } else { + needSeperator = true; + } + this._createEditEdgeButton(locale); + } + + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.deleteNode !== false) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } + } + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + + // refresh this bar based on what has been selected + this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); } - this.iconsCreated = true; + // redraw to show any possible changes + this.body.emitter.emit('_redraw'); } }, { - key: 'bindToRedraw', - value: function bindToRedraw(action) { - if (this.boundFunctions[action] === undefined) { - this.boundFunctions[action] = this[action].bind(this); - this.body.emitter.on('initRedraw', this.boundFunctions[action]); - this.body.emitter.emit('_startRendering'); + key: 'addNodeMode', + + /** + * Create the toolbar for adding Nodes + * + * @private + */ + value: function addNodeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - } - }, { - key: 'unbindFromRedraw', - value: function unbindFromRedraw(action) { - if (this.boundFunctions[action] !== undefined) { - this.body.emitter.off('initRedraw', this.boundFunctions[action]); - this.body.emitter.emit('_stopRendering'); - delete this.boundFunctions[action]; + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + this.inMode = 'addNode'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } + + this._temporaryBindEvent('click', this._performAddNode.bind(this)); } }, { - key: '_fit', + key: 'editNode', /** - * this stops all movement induced by the navigation buttons + * call the bound function to handle the editing of the node. The node has to be selected. * * @private */ - value: function _fit() { - if (new Date().valueOf() - this.touchTime > 700) { - // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) - this.body.emitter.emit('fit', { duration: 700 }); - this.touchTime = new Date().valueOf(); + value: function editNode() { + var _this2 = this; + + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + var node = this.selectionHandler._getSelectedNode(); + if (node !== undefined) { + this.inMode = 'editNode'; + if (typeof this.options.editNode === 'function') { + if (node.isCluster !== true) { + var data = util.deepExtend({}, node.options, true); + data.x = node.x; + data.y = node.y; + + if (this.options.editNode.length === 2) { + this.options.editNode(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'editNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this2.body.data.nodes.getDataSet().update(finalizedData); + } + _this2.showManipulatorToolbar(); + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); + } + } else { + alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); + } + } else { + throw new Error('No function has been configured to handle the editing of nodes.'); + } + } else { + this.showManipulatorToolbar(); } } }, { - key: '_stopMovement', + key: 'addEdgeMode', /** - * this stops all movement induced by the navigation buttons + * create the toolbar to connect nodes * * @private */ - value: function _stopMovement() { - for (var boundAction in this.boundFunctions) { - if (this.boundFunctions.hasOwnProperty(boundAction)) { - this.body.emitter.off('initRedraw', this.boundFunctions[boundAction]); - this.body.emitter.emit('_stopRendering'); - } + value: function addEdgeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - this.boundFunctions = {}; - } - }, { - key: '_moveUp', - value: function _moveUp() { - this.body.view.translation.y += this.options.keyboard.speed.y; - } - }, { - key: '_moveDown', - value: function _moveDown() { - this.body.view.translation.y -= this.options.keyboard.speed.y; - } - }, { - key: '_moveLeft', - value: function _moveLeft() { - this.body.view.translation.x += this.options.keyboard.speed.x; - } - }, { - key: '_moveRight', - value: function _moveRight() { - this.body.view.translation.x -= this.options.keyboard.speed.x; - } - }, { - key: '_zoomIn', - value: function _zoomIn() { - this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; - this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); - } - }, { - key: '_zoomOut', - value: function _zoomOut() { - this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; - this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + this.inMode = 'addEdge'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + // temporarily overload functions + this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); + this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); + this._temporaryBindUI('onDrag', this._dragControlNode.bind(this)); + this._temporaryBindUI('onRelease', this._finishConnect.bind(this)); + + this._temporaryBindUI('onDragStart', function () {}); + this._temporaryBindUI('onHold', function () {}); } }, { - key: 'configureKeyboardBindings', + key: 'editEdgeMode', /** - * bind all keys using keycharm. + * create the toolbar to edit edges + * + * @private */ - value: function configureKeyboardBindings() { - var _this2 = this; + value: function editEdgeMode() { + var _this3 = this; - if (this.keycharm !== undefined) { - this.keycharm.destroy(); + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - if (this.options.keyboard.enabled === true) { - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: true }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); - } + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - this.keycharm.reset(); + this.inMode = 'editEdge'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); - if (this.activated === true) { - this.keycharm.bind('up', function () { - _this2.bindToRedraw('_moveUp'); - }, 'keydown'); - this.keycharm.bind('down', function () { - _this2.bindToRedraw('_moveDown'); - }, 'keydown'); - this.keycharm.bind('left', function () { - _this2.bindToRedraw('_moveLeft'); - }, 'keydown'); - this.keycharm.bind('right', function () { - _this2.bindToRedraw('_moveRight'); - }, 'keydown'); - this.keycharm.bind('=', function () { - _this2.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('num+', function () { - _this2.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('num-', function () { - _this2.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind('-', function () { - _this2.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind('[', function () { - _this2.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind(']', function () { - _this2.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('pageup', function () { - _this2.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('pagedown', function () { - _this2.bindToRedraw('_zoomOut'); - }, 'keydown'); + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - this.keycharm.bind('up', function () { - _this2.unbindFromRedraw('_moveUp'); - }, 'keyup'); - this.keycharm.bind('down', function () { - _this2.unbindFromRedraw('_moveDown'); - }, 'keyup'); - this.keycharm.bind('left', function () { - _this2.unbindFromRedraw('_moveLeft'); - }, 'keyup'); - this.keycharm.bind('right', function () { - _this2.unbindFromRedraw('_moveRight'); - }, 'keyup'); - this.keycharm.bind('=', function () { - _this2.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('num+', function () { - _this2.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('num-', function () { - _this2.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind('-', function () { - _this2.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind('[', function () { - _this2.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind(']', function () { - _this2.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('pageup', function () { - _this2.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('pagedown', function () { - _this2.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - } - } - } - }]); - - return NavigationHandler; - })(); + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + if (this.edgeBeingEditedId !== undefined) { + (function () { + var edge = _this3.body.edges[_this3.edgeBeingEditedId]; - exports['default'] = NavigationHandler; - module.exports = exports['default']; + // create control nodes + var controlNodeFrom = _this3._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = _this3._getNewTargetNode(edge.to.x, edge.to.y); -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + _this3.temporaryIds.nodes.push(controlNodeFrom.id); + _this3.temporaryIds.nodes.push(controlNodeTo.id); - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + _this3.body.nodes[controlNodeFrom.id] = controlNodeFrom; + _this3.body.nodeIndices.push(controlNodeFrom.id); + _this3.body.nodes[controlNodeTo.id] = controlNodeTo; + _this3.body.nodeIndices.push(controlNodeTo.id); - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + _this3._temporaryBindUI('onTouch', _this3._controlNodeTouch.bind(_this3)); // used to get the position + _this3._temporaryBindUI('onTap', function () {}); // disabled + _this3._temporaryBindUI('onHold', function () {}); // disabled + _this3._temporaryBindUI('onDragStart', _this3._controlNodeDragStart.bind(_this3)); // used to select control node + _this3._temporaryBindUI('onDrag', _this3._controlNodeDrag.bind(_this3)); // used to drag control node + _this3._temporaryBindUI('onDragEnd', _this3._controlNodeDragEnd.bind(_this3)); // used to connect or revert control nodes + _this3._temporaryBindUI('onMouseMove', function () {}); // disabled - var TYPE_FUNCTION = 'function'; + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + _this3._temporaryBindEvent('beforeDrawing', function (ctx) { + var positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; + } + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; + } + }); - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + _this3.body.emitter.emit('_redraw'); + })(); + } else { + this.showManipulatorToolbar(); + } + } + }, { + key: 'deleteSelected', - /** - * set a timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} - */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + /** + * delete everything in the selection + * + * @private + */ + value: function deleteSelected() { + var _this4 = this; - /** - * if the argument is an array, we want to execute the fn on each entry - * if it aint an array we don't want to do a thing. - * this is used by all the methods that accept a single and array argument. - * @param {*|Array} arg - * @param {String} fn - * @param {Object} [context] - * @returns {Boolean} - */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; - } - return false; - } + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context - */ - function each(obj, iterator, context) { - var i; + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - if (!obj) { - return; - } + this.inMode = 'delete'; + var selectedNodes = this.selectionHandler.getSelectedNodes(); + var selectedEdges = this.selectionHandler.getSelectedEdges(); + var deleteFunction = undefined; + if (selectedNodes.length > 0) { + for (var i = 0; i < selectedNodes.length; i++) { + if (this.body.nodes[selectedNodes[i]].isCluster === true) { + alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); + return; + } + } - if (obj.forEach) { - obj.forEach(iterator, context); - } else if (obj.length !== undefined) { - i = 0; - while (i < obj.length) { - iterator.call(context, obj[i], i, obj); - i++; + if (typeof this.options.deleteNode === 'function') { + deleteFunction = this.options.deleteNode; } - } else { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } else if (selectedEdges.length > 0) { + if (typeof this.options.deleteEdge === 'function') { + deleteFunction = this.options.deleteEdge; } - } - } + } - /** - * extend object. - * means that properties in dest will be overwritten by the ones in src. - * @param {Object} dest - * @param {Object} src - * @param {Boolean} [merge] - * @returns {Object} dest - */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; + if (typeof deleteFunction === 'function') { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length === 2) { + deleteFunction(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'delete') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this4.body.data.edges.getDataSet().remove(finalizedData.edges); + _this4.body.data.nodes.getDataSet().remove(finalizedData.nodes); + _this4.body.emitter.emit('startSimulation'); + _this4.showManipulatorToolbar(); + } else { + _this4.body.emitter.emit('startSimulation'); + _this4.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for delete does not support two arguments (data, callback)'); } - i++; - } - return dest; - } - - /** - * merge the values from src in the dest. - * means that properties that exist in dest will not be overwritten by src - * @param {Object} dest - * @param {Object} src - * @returns {Object} dest - */ - function merge(dest, src) { - return extend(dest, src, true); - } - - /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] - */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; - - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; - - if (properties) { - extend(childP, properties); - } - } - - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } - - /** - * let a boolean value also be a function that must return a boolean - * this first item in args will be used as the context - * @param {Boolean|Function} val - * @param {Array} [args] - * @returns {Boolean} - */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); + } else { + this.body.data.edges.getDataSet().remove(selectedEdges); + this.body.data.nodes.getDataSet().remove(selectedNodes); + this.body.emitter.emit('startSimulation'); + this.showManipulatorToolbar(); + } } - return val; - } - - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + }, { + key: '_setup', - /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } + //********************************************** PRIVATE ***************************************// - /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + /** + * draw or remove the DOM + * @private + */ + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; - /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found - */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); } - node = node.parentNode; - } - return false; - } - - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } - - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + } else { + this._removeManipulationDOM(); - /** - * find if a array contains the object using indexOf or a simple polyFill - * @param {Array} src - * @param {String} find - * @param {String} [findByKey] - * @return {Boolean|Number} false when not found, or the index - */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); - } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; - } - return -1; + // disable the gui + this.guiEnabled = false; + } } - } - - /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} - */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } - - /** - * unique array with objects based on a key (like 'id') or just by the array's value - * @param {Array} src [{id:1},{id:2},{id:1}] - * @param {String} [key] - * @param {Boolean} [sort=False] - * @returns {Array} [{id:1},{id:2}] - */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; + }, { + key: '_createWrappers', - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); + /** + * create the div overlays that contain the DOM + * @private + */ + value: function _createWrappers() { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement('div'); + this.manipulationDiv.className = 'vis-manipulation'; + if (this.editMode === true) { + this.manipulationDiv.style.display = 'block'; + } else { + this.manipulationDiv.style.display = 'none'; } - values[i] = val; - i++; - } + this.canvas.frame.appendChild(this.manipulationDiv); + } - if (sort) { - if (!key) { - results = results.sort(); + // container for the edit button. + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement('div'); + this.editModeDiv.className = 'vis-edit-mode'; + if (this.editMode === true) { + this.editModeDiv.style.display = 'none'; } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); + this.editModeDiv.style.display = 'block'; } - } + this.canvas.frame.appendChild(this.editModeDiv); + } - return results; - } + // container for the close div button + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement('div'); + this.closeDiv.className = 'vis-close'; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.canvas.frame.appendChild(this.closeDiv); + } + } + }, { + key: '_getNewTargetNode', - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); + /** + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} + * @private + */ + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + controlNodeStyle.id = 'targetNode' + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; - if (prop in obj) { - return prop; - } - i++; + return this.body.functions.createNode(controlNodeStyle); } - return undefined; - } - - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } - - /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} - */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } + }, { + key: '_createEditButton', - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + /** + * Create the edit button + */ + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); - var SUPPORT_TOUCH = ('ontouchstart' in window); - var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; - var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); + // reset the manipulationDOM + this.manipulationDOM = {}; - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); - var COMPUTE_INTERVAL = 25; + // create the contents for the editMode button + var locale = this.options.locales[this.options.locale]; + var button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']); + this.editModeDiv.appendChild(button); - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); + } + }, { + key: '_clean', - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + /** + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * @private + */ + value: function _clean() { + // not in mode + this.inMode = false; - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor - */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - // smaller wrapper around the handler, for the scope and the enabled state of the manager, - // so when disabled the input events are completely bypassed. - this.domHandler = function(ev) { - if (boolOrFn(manager.options.enable, [manager])) { - self.handler(ev); - } - }; + // restore overloaded UI functions + this._unbindTemporaryUIs(); - this.init(); + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - } + // restore the physics if required + this.body.emitter.emit('restorePhysics'); + } + }, { + key: '_cleanManipulatorHammers', - Input.prototype = { /** - * should handle the inputEvent data and trigger the callback - * @virtual + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * @private */ - handler: function() { }, + value: function _cleanManipulatorHammers() { + // _clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].destroy(); + } + this.manipulationHammers = []; + } + } + }, { + key: '_removeManipulationDOM', /** - * bind the events + * Remove all DOM elements created by this module. + * @private */ - init: function() { - this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - }, + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - /** - * unbind the events - */ - destroy: function() { - this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - } - }; + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); - /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} - */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; + // remove the manipulation divs + if (this.manipulationDiv) { + this.canvas.frame.removeChild(this.manipulationDiv); + } + if (this.editModeDiv) { + this.canvas.frame.removeChild(this.editModeDiv); + } + if (this.closeDiv) { + this.canvas.frame.removeChild(this.manipulationDiv); + } - if (inputClass) { - Type = inputClass; - } else if (SUPPORT_POINTER_EVENTS) { - Type = PointerEventInput; - } else if (SUPPORT_ONLY_TOUCH) { - Type = TouchInput; - } else if (!SUPPORT_TOUCH) { - Type = MouseInput; - } else { - Type = TouchMouseInput; + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; } - return new (Type)(manager, inputHandler); - } - - /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input - */ - function inputHandler(manager, eventType, input) { - var pointersLen = input.pointers.length; - var changedPointersLen = input.changedPointers.length; - var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); - var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); + }, { + key: '_createSeperator', - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + /** + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index + * @private + */ + value: function _createSeperator() { + var index = arguments[0] === undefined ? 1 : arguments[0]; - if (isFirst) { - manager.session = {}; + this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); + this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; + this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); } + }, { + key: '_createAddNodeButton', - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - - // compute scale, rotation etc - computeInputData(manager, input); - - // emit secret event - manager.emit('hammer.input', input); - - manager.recognize(input); - manager.session.prevInput = input; - } - - /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input - */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + // ---------------------- DOM functions for buttons --------------------------// - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); + value: function _createAddNodeButton(locale) { + var button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addNodeMode.bind(this)); } - - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; + }, { + key: '_createAddEdgeButton', + value: function _createAddEdgeButton(locale) { + var button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); } - - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; - - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); - - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); - - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - - computeIntervalInputData(session, input); - - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; + }, { + key: '_createEditNodeButton', + value: function _createEditNodeButton(locale) { + var button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editNode.bind(this)); } - input.target = target; - } - - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; - - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; - - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; + }, { + key: '_createEditEdgeButton', + value: function _createEditEdgeButton(locale) { + var button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); } - - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } - - /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input - */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; - - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; - - var v = getVelocity(deltaTime, deltaX, deltaY); - velocityX = v.x; - velocityY = v.y; - velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; - direction = getDirection(deltaX, deltaY); - - session.lastInterval = input; - } else { - // use latest velocity info if it doesn't overtake a minimum period - velocity = last.velocity; - velocityX = last.velocityX; - velocityY = last.velocityY; - direction = last.direction; + }, { + key: '_createDeleteButton', + value: function _createDeleteButton(locale) { + var button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.deleteSelected.bind(this)); } - - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } - - /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData - */ - function simpleCloneInputData(input) { - // make a simple copy of the pointers because we will get a reference if we don't - // we only need clientXY for the calculations - var pointers = []; - var i = 0; - while (i < input.pointers.length) { - pointers[i] = { - clientX: round(input.pointers[i].clientX), - clientY: round(input.pointers[i].clientY) - }; - i++; + }, { + key: '_createBackButton', + value: function _createBackButton(locale) { + var button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); } + }, { + key: '_createButton', + value: function _createButton(id, className, label) { + var labelClassName = arguments[3] === undefined ? 'vis-label' : arguments[3]; - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } - - /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties - */ - function getCenter(pointers) { - var pointersLength = pointers.length; - - // no need to loop when only one touch - if (pointersLength === 1) { - return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) - }; + this.manipulationDOM[id + 'Div'] = document.createElement('div'); + this.manipulationDOM[id + 'Div'].className = className; + this.manipulationDOM[id + 'Label'] = document.createElement('div'); + this.manipulationDOM[id + 'Label'].className = labelClassName; + this.manipulationDOM[id + 'Label'].innerHTML = label; + this.manipulationDOM[id + 'Div'].appendChild(this.manipulationDOM[id + 'Label']); + return this.manipulationDOM[id + 'Div']; } - - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; + }, { + key: '_createDescription', + value: function _createDescription(label) { + this.manipulationDiv.appendChild(this._createButton('description', 'vis-button vis-none', label)); } + }, { + key: '_temporaryBindEvent', - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } + // -------------------------- End of DOM functions for buttons ------------------------------// - /** - * calculate the velocity between two points. unit is in px per ms. - * @param {Number} deltaTime - * @param {Number} x - * @param {Number} y - * @return {Object} velocity `x` and `y` - */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } + /** + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction + * @private + */ + value: function _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); + this.body.emitter.on(event, newFunction); + } + }, { + key: '_temporaryBindUI', - /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction - */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; + /** + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction + * @private + */ + value: function _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; + } else { + throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); + } } + }, { + key: '_unbindTemporaryUIs', - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + /** + * Restore the overridden UI functions to their original state. + * + * @private + */ + value: function _unbindTemporaryUIs() { + for (var functionName in this.temporaryUIFunctions) { + if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { + this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; + delete this.temporaryUIFunctions[functionName]; + } + } + this.temporaryUIFunctions = {}; } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } + }, { + key: '_unbindTemporaryEvents', - /** - * calculate the absolute distance between two points - * @param {Object} p1 {x, y} - * @param {Object} p2 {x, y} - * @param {Array} [props] containing x and y keys - * @return {Number} distance - */ - function getDistance(p1, p2, props) { - if (!props) { - props = PROPS_XY; + /** + * Unbind the events created by _temporaryBindEvent + * @private + */ + value: function _unbindTemporaryEvents() { + for (var i = 0; i < this.temporaryEventFunctions.length; i++) { + var eventName = this.temporaryEventFunctions[i].event; + var boundFunction = this.temporaryEventFunctions[i].boundFunction; + this.body.emitter.off(eventName, boundFunction); + } + this.temporaryEventFunctions = []; } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - - return Math.sqrt((x * x) + (y * y)); - } + }, { + key: '_bindHammerToDiv', - /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle - */ - function getAngle(p1, p2, props) { - if (!props) { - props = PROPS_XY; + /** + * Bind an hammer instance to a DOM element. + * @param domElement + * @param funct + */ + value: function _bindHammerToDiv(domElement, boundFunction) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, boundFunction); + this.manipulationHammers.push(hammer); } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - return Math.atan2(y, x) * 180 / Math.PI; - } + }, { + key: '_cleanupTemporaryNodesAndEdges', - /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation - */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } + /** + * Neatly clean up temporary edges and nodes + * @private + */ + value: function _cleanupTemporaryNodesAndEdges() { + // _clean temporary edges + for (var i = 0; i < this.temporaryIds.edges.length; i++) { + this.body.edges[this.temporaryIds.edges[i]].disconnect(); + delete this.body.edges[this.temporaryIds.edges[i]]; + var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); + if (indexTempEdge !== -1) { + this.body.edgeIndices.splice(indexTempEdge, 1); + } + } - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } + // _clean temporary nodes + for (var i = 0; i < this.temporaryIds.nodes.length; i++) { + delete this.body.nodes[this.temporaryIds.nodes[i]]; + var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); + if (indexTempNode !== -1) { + this.body.nodeIndices.splice(indexTempNode, 1); + } + } - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; + this.temporaryIds = { nodes: [], edges: [] }; + } + }, { + key: '_controlNodeTouch', - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// - /** - * Mouse events input - * @constructor - * @extends Input - */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; + /** + * the touch is used to get the position of the initial click + * @param event + * @private + */ + value: function _controlNodeTouch(event) { + this.selectionHandler.unselectAll(); + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + } + }, { + key: '_controlNodeDragStart', - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state + /** + * the drag start is used to mark one of the control nodes as selected. + * @param event + * @private + */ + value: function _controlNodeDragStart(event) { + var pointer = this.lastTouch; + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + var to = this.body.nodes[this.temporaryIds.nodes[1]]; + var edge = this.body.edges[this.edgeBeingEditedId]; + this.selectedControlNode = undefined; - Input.apply(this, arguments); - } + var fromSelect = from.isOverlappingWith(pointerObj); + var toSelect = to.isOverlappingWith(pointerObj); + + if (fromSelect === true) { + this.selectedControlNode = from; + edge.edgeType.from = from; + } else if (toSelect === true) { + this.selectedControlNode = to; + edge.edgeType.to = to; + } + + this.body.emitter.emit('_redraw'); + } + }, { + key: '_controlNodeDrag', - inherit(MouseInput, Input, { /** - * handle mouse events - * @param {Object} ev + * dragging the control nodes or the canvas + * @param event + * @private */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; + value: function _controlNodeDrag(event) { + this.body.emitter.emit('disablePhysics'); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } + if (this.selectedControlNode !== undefined) { + this.selectedControlNode.x = pos.x; + this.selectedControlNode.y = pos.y; + } else { + // if the drag was not started properly because the click started outside the network div, start it now. + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + } + this.body.emitter.emit('_redraw'); + } + }, { + key: '_controlNodeDragEnd', - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + /** + * connecting or restoring the control nodes. + * @param event + * @private + */ + value: function _controlNodeDragEnd(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var edge = this.body.edges[this.edgeBeingEditedId]; - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (overlappingNodeIds[i] !== this.selectedControlNode.id) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; } + } - if (eventType & INPUT_END) { - this.pressed = false; + // perform the connection + if (node !== undefined && this.selectedControlNode !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + if (this.selectedControlNode.id === from.id) { + this._performEditEdge(node.id, edge.to.id); + } else { + this._performEditEdge(edge.from.id, node.id); + } } - - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); + } else { + edge.updateEdgeType(); + this.body.emitter.emit('restorePhysics'); + } + this.body.emitter.emit('_redraw'); } - }); - - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; + }, { + key: '_handleConnect', - // in IE10 the pointer types is defined as an enum - var IE10_POINTER_TYPE_ENUM = { - 2: INPUT_TYPE_TOUCH, - 3: INPUT_TYPE_PEN, - 4: INPUT_TYPE_MOUSE, - 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 - }; + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + value: function _handleConnect(event) { + // check to avoid double fireing of this function. + if (new Date().valueOf() - this.touchTime > 100) { + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } + var pointer = this.lastTouch; + var node = this.selectionHandler.getNodeAt(pointer); - /** - * Pointer events input - * @constructor - * @extends Input - */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + // create a node the temporary line can look at + var targetNode = this._getNewTargetNode(node.x, node.y); + this.body.nodes[targetNode.id] = targetNode; + this.body.nodeIndices.push(targetNode.id); - Input.apply(this, arguments); + // create a temporary edge + var connectionEdge = this.body.functions.createEdge({ + id: 'connectionEdge' + util.randomUUID(), + from: node.id, + to: targetNode.id, + physics: false, + smooth: { + enabled: true, + type: 'continuous', + roundness: 0.5 + } + }); + this.body.edges[connectionEdge.id] = connectionEdge; + this.body.edgeIndices.push(connectionEdge.id); - this.store = (this.manager.session.pointerEvents = []); - } + this.temporaryIds.nodes.push(targetNode.id); + this.temporaryIds.edges.push(connectionEdge.id); + } + } + this.touchTime = new Date().valueOf(); + } + } + }, { + key: '_dragControlNode', + value: function _dragControlNode(event) { + var pointer = this.body.functions.getPointer(event.center); + if (this.temporaryIds.nodes[0] !== undefined) { + var targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. + targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); + targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); + this.body.emitter.emit('_redraw'); + } else { + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + } + } + }, { + key: '_finishConnect', - inherit(PointerEventInput, Input, { /** - * handle mouse events - * @param {Object} ev + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event + * @private */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; - - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - - var isTouch = (pointerType == INPUT_TYPE_TOUCH); - - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - // start and mouse must be down - if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { - if (storeIndex < 0) { - store.push(ev); - storeIndex = store.length - 1; - } - } else if (eventType & (INPUT_END | INPUT_CANCEL)) { - removePointer = true; - } + // remember the edge id + var connectFromId = undefined; + if (this.temporaryIds.edges[0] !== undefined) { + connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; + } - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; + // get the overlapping node but NOT the temporary node; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + // if the node id is NOT a temporary node, accept the node. + if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; } + } - // update the event in the store - store[storeIndex] = ev; - - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + // clean temporary nodes and edges. + this._cleanupTemporaryNodesAndEdges(); - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); + // perform the connection + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performAddEdge(connectFromId, node.id); + } } + } + this.body.emitter.emit('_redraw'); } - }); - - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; + }, { + key: '_performAddNode', - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// - /** - * Touch events input - * @constructor - * @extends Input - */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + // ------------------------------ Performing all the actual data manipulation ------------------------// - Input.apply(this, arguments); - } + /** + * Adds a node on the specified location + */ + value: function _performAddNode(clickData) { + var _this5 = this; - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: 'new' + }; - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; + if (typeof this.options.addNode === 'function') { + if (this.options.addNode.length === 2) { + this.options.addNode(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this5.body.data.nodes.getDataSet().add(finalizedData); + _this5.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for add does not support two arguments (data,callback)'); + this.showManipulatorToolbar(); } + } else { + this.body.data.nodes.getDataSet().add(defaultData); + this.showManipulatorToolbar(); + } + } + }, { + key: '_performAddEdge', - if (!this.started) { - return; + /** + * connect two nodes with a new edge. + * + * @private + */ + value: function _performAddEdge(sourceNodeId, targetNodeId) { + var _this6 = this; + + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (typeof this.options.addEdge === 'function') { + if (this.options.addEdge.length === 2) { + this.options.addEdge(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this6.inMode === 'addEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this6.body.data.edges.add(finalizedData); + _this6.selectionHandler.unselectAll(); + _this6.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for connect does not support two arguments (data,callback)'); } + } else { + this.body.data.edges.add(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } + } + }, { + key: '_performEditEdge', - var touches = normalizeSingleTouches.call(this, ev, type); + /** + * connect two nodes with a new edge. + * + * @private + */ + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this7 = this; - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; + var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; + if (typeof this.options.editEdge === 'function') { + if (this.options.editEdge.length === 2) { + this.options.editEdge(defaultData, function (finalizedData) { + if (finalizedData === null || finalizedData === undefined || _this7.inMode !== 'editEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this7.body.edges[defaultData.id].updateEdgeType(); + _this7.body.emitter.emit('_redraw'); + } else { + _this7.body.data.edges.update(finalizedData); + _this7.selectionHandler.unselectAll(); + _this7.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + } else { + this.body.data.edges.update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } } - }); + }]); - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); + return ManipulationSystem; + })(); - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } + exports['default'] = ManipulationSystem; + module.exports = exports['default']; - return [all, changed]; - } +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; + 'use strict'; - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; + Object.defineProperty(exports, '__esModule', { + value: true + }); - /** - * Multi-user touch events input - * @constructor - * @extends Input - */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - Input.apply(this, arguments); - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } + var util = __webpack_require__(2); + var Hammer = __webpack_require__(31); + var hammerUtil = __webpack_require__(34); + var keycharm = __webpack_require__(49); - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; + _classCallCheck(this, NavigationHandler); - // when there is only one touch, the process can be simplified - if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { - targetIds[allTouches[0].identifier] = true; - return [allTouches, allTouches]; - } + this.body = body; + this.canvas = canvas; - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); + this.body.emitter.on('release', function () { + _this._stopMovement(); + }); + this.body.emitter.on('activate', function () { + _this.activated = true;_this.configureKeyboardBindings(); + }); + this.body.emitter.on('deactivate', function () { + _this.activated = false;_this.configureKeyboardBindings(); + }); + this.body.emitter.on('destroy', function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); + } }); - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } + this.options = {}; + } - // filter changed touches to only contain touches that exist in the collected target ids - i = 0; - while (i < changedTouches.length) { - if (targetIds[changedTouches[i].identifier]) { - changedTargetTouches.push(changedTouches[i]); + _createClass(NavigationHandler, [{ + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + this.options = options; + this.create(); + } + } + }, { + key: 'create', + value: function create() { + if (this.options.navigationButtons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); + } - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; - } - i++; + this.configureKeyboardBindings(); } + }, { + key: 'cleanNavigation', + value: function cleanNavigation() { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); + } + this.navigationHammers = []; + } - if (!changedTargetTouches.length) { - return; + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { + this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); + } + + this.iconsCreated = false; } + }, { + key: 'loadNavigationElements', - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + value: function loadNavigationElements() { + this.cleanNavigation(); - /** - * Combined touch and mouse input - * - * Touch has a higher priority then mouse, and while touching no mouse events are allowed. - * This because touch devices also emit mouse events while doing a touch. - * - * @constructor - * @extends Input - */ - function TouchMouseInput() { - Input.apply(this, arguments); + this.navigationDOM = {}; + var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; + var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } + this.navigationDOM['wrapper'] = document.createElement('div'); + this.navigationDOM['wrapper'].className = 'vis-navigation'; + this.canvas.frame.appendChild(this.navigationDOM['wrapper']); - inherit(TouchMouseInput, Input, { - /** - * handle mouse and touch events - * @param {Hammer} manager - * @param {String} inputEvent - * @param {Object} inputData - */ - handler: function TMEhandler(manager, inputEvent, inputData) { - var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), - isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement('div'); + this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i]; + this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); - // when we're in a touch event, so block all upcoming mouse events - // most mobile browser also emit mouseevents, right after touchstart - if (isTouch) { - this.mouse.allow = false; - } else if (isMouse && !this.mouse.allow) { - return; + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); + if (navigationDivActions[i] === '_fit') { + hammerUtil.onTouch(hammer, this._fit.bind(this)); + } else { + hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); } - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } + this.navigationHammers.push(hammer); + } - this.callback(manager, inputEvent, inputData); - }, + this.iconsCreated = true; + } + }, { + key: 'bindToRedraw', + value: function bindToRedraw(action) { + if (this.boundFunctions[action] === undefined) { + this.boundFunctions[action] = this[action].bind(this); + this.body.emitter.on('initRedraw', this.boundFunctions[action]); + this.body.emitter.emit('_startRendering'); + } + } + }, { + key: 'unbindFromRedraw', + value: function unbindFromRedraw(action) { + if (this.boundFunctions[action] !== undefined) { + this.body.emitter.off('initRedraw', this.boundFunctions[action]); + this.body.emitter.emit('_stopRendering'); + delete this.boundFunctions[action]; + } + } + }, { + key: '_fit', /** - * remove the event listeners + * this stops all movement induced by the navigation buttons + * + * @private */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); + value: function _fit() { + if (new Date().valueOf() - this.touchTime > 700) { + // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) + this.body.emitter.emit('fit', { duration: 700 }); + this.touchTime = new Date().valueOf(); + } } - }); - - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - - // magical touchAction value - var TOUCH_ACTION_COMPUTE = 'compute'; - var TOUCH_ACTION_AUTO = 'auto'; - var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented - var TOUCH_ACTION_NONE = 'none'; - var TOUCH_ACTION_PAN_X = 'pan-x'; - var TOUCH_ACTION_PAN_Y = 'pan-y'; - - /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor - */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } + }, { + key: '_stopMovement', - TouchAction.prototype = { /** - * set the touchAction value on the element or enable the polyfill - * @param {String} value + * this stops all movement induced by the navigation buttons + * + * @private */ - set: function(value) { - // find out the touch-action by the event handlers - if (value == TOUCH_ACTION_COMPUTE) { - value = this.compute(); - } - - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + value: function _stopMovement() { + for (var boundAction in this.boundFunctions) { + if (this.boundFunctions.hasOwnProperty(boundAction)) { + this.body.emitter.off('initRedraw', this.boundFunctions[boundAction]); + this.body.emitter.emit('_stopRendering'); } - this.actions = value.toLowerCase().trim(); - }, + } + this.boundFunctions = {}; + } + }, { + key: '_moveUp', + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; + } + }, { + key: '_moveDown', + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; + } + }, { + key: '_moveLeft', + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; + } + }, { + key: '_moveRight', + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; + } + }, { + key: '_zoomIn', + value: function _zoomIn() { + this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; + this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); + } + }, { + key: '_zoomOut', + value: function _zoomOut() { + this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; + this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); + } + }, { + key: 'configureKeyboardBindings', /** - * just re-set the touchAction value + * bind all keys using keycharm. */ - update: function() { - this.set(this.manager.options.touchAction); - }, + value: function configureKeyboardBindings() { + var _this2 = this; - /** - * compute the value for the touchAction property based on the recognizer's settings - * @returns {String} value - */ - compute: function() { - var actions = []; - each(this.manager.recognizers, function(recognizer) { - if (boolOrFn(recognizer.options.enable, [recognizer])) { - actions = actions.concat(recognizer.getTouchAction()); - } - }); - return cleanTouchActions(actions.join(' ')); - }, + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } - /** - * this method is called on each input cycle and provides the preventing of the browser behavior - * @param {Object} input - */ - preventDefaults: function(input) { - // not needed with native support for the touchAction property - if (NATIVE_TOUCH_ACTION) { - return; + if (this.options.keyboard.enabled === true) { + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: true }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); } - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; - - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } + this.keycharm.reset(); - var actions = this.actions; - var hasNone = inStr(actions, TOUCH_ACTION_NONE); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + if (this.activated === true) { + this.keycharm.bind('up', function () { + _this2.bindToRedraw('_moveUp'); + }, 'keydown'); + this.keycharm.bind('down', function () { + _this2.bindToRedraw('_moveDown'); + }, 'keydown'); + this.keycharm.bind('left', function () { + _this2.bindToRedraw('_moveLeft'); + }, 'keydown'); + this.keycharm.bind('right', function () { + _this2.bindToRedraw('_moveRight'); + }, 'keydown'); + this.keycharm.bind('=', function () { + _this2.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('num+', function () { + _this2.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('num-', function () { + _this2.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind('-', function () { + _this2.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind('[', function () { + _this2.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind(']', function () { + _this2.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('pageup', function () { + _this2.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('pagedown', function () { + _this2.bindToRedraw('_zoomOut'); + }, 'keydown'); - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); + this.keycharm.bind('up', function () { + _this2.unbindFromRedraw('_moveUp'); + }, 'keyup'); + this.keycharm.bind('down', function () { + _this2.unbindFromRedraw('_moveDown'); + }, 'keyup'); + this.keycharm.bind('left', function () { + _this2.unbindFromRedraw('_moveLeft'); + }, 'keyup'); + this.keycharm.bind('right', function () { + _this2.unbindFromRedraw('_moveRight'); + }, 'keyup'); + this.keycharm.bind('=', function () { + _this2.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('num+', function () { + _this2.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('num-', function () { + _this2.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind('-', function () { + _this2.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind('[', function () { + _this2.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind(']', function () { + _this2.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('pageup', function () { + _this2.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('pagedown', function () { + _this2.unbindFromRedraw('_zoomOut'); + }, 'keyup'); } - }, - - /** - * call preventDefault to prevent the browser's default behavior (scrolling in most cases) - * @param {Object} srcEvent - */ - preventSrc: function(srcEvent) { - this.manager.session.prevented = true; - srcEvent.preventDefault(); + } } - }; + }]); - /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} - */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } + return NavigationHandler; + })(); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + exports['default'] = NavigationHandler; + module.exports = exports['default']; - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; - } + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ + * + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - return TOUCH_ACTION_AUTO; - } + var TYPE_FUNCTION = 'function'; + + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * Recognizer flow explained; * - * All recognizers have the initial state of POSSIBLE when a input session starts. - * The definition of a input session is from the first input until the last input, with all it's movement in it. * - * Example session for mouse-input: mousedown -> mousemove -> mouseup - * - * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed - * which determines with state it should be. - * - * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to - * POSSIBLE to give it another change on the next cycle. - * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options + * if the argument is an array, we want to execute the fn on each entry + * if it aint an array we don't want to do a thing. + * this is used by all the methods that accept a single and array argument. + * @param {*|Array} arg + * @param {String} fn + * @param {Object} [context] + * @returns {Boolean} */ - function Recognizer(options) { - this.id = uniqueId(); - - this.manager = null; - this.options = merge(options || {}, this.defaults); - - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); - - this.state = STATE_POSSIBLE; - - this.simultaneous = {}; - this.requireFail = []; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; + } + return false; } - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, - - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + /** + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context + */ + function each(obj, iterator, context) { + var i; - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, + if (!obj) { + return; + } - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj.length !== undefined) { + i = 0; + while (i < obj.length) { + iterator.call(context, obj[i], i, obj); + i++; } - - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); + } else { + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); } - return this; - }, + } + } - /** - * drop the simultaneous link. it doesnt remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRecognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { - return this; + /** + * extend object. + * means that properties in dest will be overwritten by the ones in src. + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge] + * @returns {Object} dest + */ + function extend(dest, src, merge) { + var keys = Object.keys(src); + var i = 0; + while (i < keys.length) { + if (!merge || (merge && dest[keys[i]] === undefined)) { + dest[keys[i]] = src[keys[i]]; } + i++; + } + return dest; + } - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, + /** + * merge the values from src in the dest. + * means that properties that exist in dest will not be overwritten by src + * @param {Object} dest + * @param {Object} src + * @returns {Object} dest + */ + function merge(dest, src) { + return extend(dest, src, true); + } - /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } + /** + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] + */ + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - /** - * drop the requireFailure link. it does not remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRequireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { - return this; - } + if (properties) { + extend(childP, properties); + } + } - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, + /** + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} + */ + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, + /** + * let a boolean value also be a function that must return a boolean + * this first item in args will be used as the context + * @param {Boolean|Function} val + * @param {Array} [args] + * @returns {Boolean} + */ + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); + } + return val; + } - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; + /** + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } + /** + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; } + node = node.parentNode; + } + return false; + } - emit(); // simple 'eventName' events - - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, + /** + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found + */ + function inStr(str, find) { + return str.indexOf(find) > -1; + } - /** - * Check that all the require failure recognizers has failed, - * if true, it emits a gesture event, - * otherwise, setup the state to FAILED. - * @param {Object} input - */ - tryEmit: function(input) { - if (this.canEmit()) { - return this.emit(input); - } - // it's failing anyway - this.state = STATE_FAILED; - }, + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } - /** - * can we emit? - * @returns {boolean} - */ - canEmit: function() { + /** + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index + */ + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); + } else { var i = 0; - while (i < this.requireFail.length) { - if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { - return false; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; } i++; } - return true; - }, - - /** - * update the recognizer - * @param {Object} inputData - */ - recognize: function(inputData) { - // make a new copy of the inputData - // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); - - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } + return -1; + } + } - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } + /** + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} + */ + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } - this.state = this.process(inputDataClone); + /** + * unique array with objects based on a key (like 'id') or just by the array's value + * @param {Array} src [{id:1},{id:2},{id:1}] + * @param {String} [key] + * @param {Boolean} [sort=False] + * @returns {Array} [{id:1},{id:2}] + */ + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); + while (i < src.length) { + var val = key ? src[i][key] : src[i]; + if (inArray(values, val) < 0) { + results.push(src[i]); } - }, - - /** - * return the state of the recognizer - * the actual recognizing happens in this method - * @virtual - * @param {Object} inputData - * @returns {Const} STATE - */ - process: function(inputData) { }, // jshint ignore:line + values[i] = val; + i++; + } - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } + } - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } - }; + return results; + } /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); + + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; + + if (prop in obj) { + return prop; + } + i++; } - return ''; + return undefined; } /** - * direction cons to string - * @param {Const} direction - * @returns {String} + * get a unique id + * @returns {number} uniqueId */ - function directionStr(direction) { - if (direction == DIRECTION_DOWN) { - return 'down'; - } else if (direction == DIRECTION_UP) { - return 'up'; - } else if (direction == DIRECTION_LEFT) { - return 'left'; - } else if (direction == DIRECTION_RIGHT) { - return 'right'; - } - return ''; + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; } /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); } + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + + var SUPPORT_TOUCH = ('ontouchstart' in window); + var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; + var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); + + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; + + var COMPUTE_INTERVAL = 25; + + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; + + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; + + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; + /** - * This recognizer is just used as a base for the simple attribute recognizers. + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} * @constructor - * @extends Recognizer */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; + + // smaller wrapper around the handler, for the scope and the enabled state of the manager, + // so when disabled the input events are completely bypassed. + this.domHandler = function(ev) { + if (boolOrFn(manager.options.enable, [manager])) { + self.handler(ev); + } + }; + + this.init(); + } - inherit(AttrRecognizer, Recognizer, { + Input.prototype = { /** - * @namespace - * @memberof AttrRecognizer + * should handle the inputEvent data and trigger the callback + * @virtual */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + handler: function() { }, /** - * Used to check if it the recognizer receives valid input, like input.distance > 10. - * @memberof AttrRecognizer - * @param {Object} input - * @returns {Boolean} recognized + * bind the events */ - attrTest: function(input) { - var optionPointers = this.options.pointers; - return optionPointers === 0 || input.pointers.length === optionPointers; + init: function() { + this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); }, /** - * Process the input and return the state for the recognizer - * @memberof AttrRecognizer - * @param {Object} input - * @returns {*} State + * unbind the events */ - process: function(input) { - var state = this.state; - var eventType = input.eventType; + destroy: function() { + this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + } + }; - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + /** + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} + */ + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - // on cancel input and we've recognized before, return STATE_CANCELLED - if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { - return state | STATE_CANCELLED; - } else if (isRecognized || isValid) { - if (eventType & INPUT_END) { - return state | STATE_ENDED; - } else if (!(state & STATE_BEGAN)) { - return STATE_BEGAN; - } - return state | STATE_CHANGED; - } - return STATE_FAILED; + if (inputClass) { + Type = inputClass; + } else if (SUPPORT_POINTER_EVENTS) { + Type = PointerEventInput; + } else if (SUPPORT_ONLY_TOUCH) { + Type = TouchInput; + } else if (!SUPPORT_TOUCH) { + Type = MouseInput; + } else { + Type = TouchMouseInput; } - }); + return new (Type)(manager, inputHandler); + } /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + function inputHandler(manager, eventType, input) { + var pointersLen = input.pointers.length; + var changedPointersLen = input.changedPointers.length; + var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); + var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - this.pX = null; - this.pY = null; + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; + + if (isFirst) { + manager.session = {}; + } + + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; + + // compute scale, rotation etc + computeInputData(manager, input); + + // emit secret event + manager.emit('hammer.input', input); + + manager.recognize(input); + manager.session.prevInput = input; } - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + /** + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input + */ + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); - } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); - } - return actions; - }, + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } - directionTest: function(input) { - var options = this.options; - var hasMoved = true; - var distance = input.distance; - var direction = input.direction; - var x = input.deltaX; - var y = input.deltaY; + // to compute scale and rotation we need to store the multiple touches + if (pointersLength > 1 && !session.firstMultiple) { + session.firstMultiple = simpleCloneInputData(input); + } else if (pointersLength === 1) { + session.firstMultiple = false; + } - // lock to axis? - if (!(direction & options.direction)) { - if (options.direction & DIRECTION_HORIZONTAL) { - direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; - hasMoved = x != this.pX; - distance = Math.abs(input.deltaX); - } else { - direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; - hasMoved = y != this.pY; - distance = Math.abs(input.deltaY); - } - } - input.direction = direction; + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - return hasMoved && distance > options.threshold && direction & options.direction; - }, + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - this._super.emit.call(this, input); + computeIntervalInputData(session, input); + + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; } - }); + input.target = target; + } - /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer - */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; + + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; + + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; + } + + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); } - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + var v = getVelocity(deltaTime, deltaX, deltaY); + velocityX = v.x; + velocityY = v.y; + velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; + direction = getDirection(deltaX, deltaY); - emit: function(input) { - this._super.emit.call(this, input); - if (input.scale !== 1) { - var inOut = input.scale < 1 ? 'in' : 'out'; - this.manager.emit(this.options.event + inOut, input); - } + session.lastInterval = input; + } else { + // use latest velocity info if it doesn't overtake a minimum period + velocity = last.velocity; + velocityX = last.velocityX; + velocityY = last.velocityY; + direction = last.direction; } - }); + + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + function simpleCloneInputData(input) { + // make a simple copy of the pointers because we will get a reference if we don't + // we only need clientXY for the calculations + var pointers = []; + var i = 0; + while (i < input.pointers.length) { + pointers[i] = { + clientX: round(input.pointers[i].clientX), + clientY: round(input.pointers[i].clientY) + }; + i++; + } - this._timer = null; - this._input = null; + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; } - inherit(PressRecognizer, Recognizer, { - /** - * @namespace - * @memberof PressRecognizer - */ - defaults: { - event: 'press', - pointers: 1, - time: 500, // minimal time of the pointer to be pressed - threshold: 5 // a minimal movement is ok, but keep it low - }, + /** + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties + */ + function getCenter(pointers) { + var pointersLength = pointers.length; - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; + } - process: function(input) { - var options = this.options; - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTime = input.deltaTime > options.time; + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; + } + + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } - this._input = input; + /** + * calculate the velocity between two points. unit is in px per ms. + * @param {Number} deltaTime + * @param {Number} x + * @param {Number} y + * @return {Object} velocity `x` and `y` + */ + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { - this.reset(); - } else if (input.eventType & INPUT_START) { - this.reset(); - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.time, this); - } else if (input.eventType & INPUT_END) { - return STATE_RECOGNIZED; - } - return STATE_FAILED; - }, + /** + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction + */ + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; + } - reset: function() { - clearTimeout(this._timer); - }, + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + /** + * calculate the absolute distance between two points + * @param {Object} p1 {x, y} + * @param {Object} p2 {x, y} + * @param {Array} [props] containing x and y keys + * @return {Number} distance + */ + function getDistance(p1, p2, props) { + if (!props) { + props = PROPS_XY; + } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); - } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); - } + return Math.sqrt((x * x) + (y * y)); + } + + /** + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle + */ + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } - }); + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); } - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, + /** + * calculate the scale factor between two pointersets + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} scale + */ + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); + } - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } - }); + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; /** - * Swipe - * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * Mouse events input * @constructor - * @extends AttrRecognizer + * @extends Input */ - function SwipeRecognizer() { - AttrRecognizer.apply(this, arguments); + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; + + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state + + Input.apply(this, arguments); } - inherit(SwipeRecognizer, AttrRecognizer, { + inherit(MouseInput, Input, { /** - * @namespace - * @memberof SwipeRecognizer + * handle mouse events + * @param {Object} ev */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, - - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; + } - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; } - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); + if (eventType & INPUT_END) { + this.pressed = false; } - this.manager.emit(this.options.event, input); + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } }); + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL + }; + + // in IE10 the pointer types is defined as an enum + var IE10_POINTER_TYPE_ENUM = { + 2: INPUT_TYPE_TOUCH, + 3: INPUT_TYPE_PEN, + 4: INPUT_TYPE_MOUSE, + 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + }; + + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; + + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } + /** - * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur - * between the given interval and position. The delay option can be used to recognize multi-taps without firing - * a single tap. - * - * The eventData from the emitted event contains the property `tapCount`, which contains the amount of - * multi-taps being recognized. + * Pointer events input * @constructor - * @extends Recognizer + * @extends Input */ - function TapRecognizer() { - Recognizer.apply(this, arguments); + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + Input.apply(this, arguments); - this._timer = null; - this._input = null; - this.count = 0; + this.store = (this.manager.session.pointerEvents = []); } - inherit(TapRecognizer, Recognizer, { + inherit(PointerEventInput, Input, { /** - * @namespace - * @memberof PinchRecognizer + * handle mouse events + * @param {Object} ev */ - defaults: { - event: 'tap', - pointers: 1, - taps: 1, - interval: 300, // max time between the multi-tap taps - time: 250, // max time of the pointer to be down (like finger on the screen) - threshold: 2, // a minimal movement is ok, but keep it low - posThreshold: 10 // a multi-tap can be a bit off the initial position - }, - - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, - - process: function(input) { - var options = this.options; - - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; - - this.reset(); - - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (validMovement && validTouchTime && validPointers) { - if (input.eventType != INPUT_END) { - return this.failTimeout(); - } + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - this.pTime = input.timeStamp; - this.pCenter = input.center; + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; + // start and mouse must be down + if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { + if (storeIndex < 0) { + store.push(ev); + storeIndex = store.length - 1; } + } else if (eventType & (INPUT_END | INPUT_CANCEL)) { + removePointer = true; + } - this._input = input; - - // if tap count matches we have recognized it, - // else it has began recognizing... - var tapCount = this.count % options.taps; - if (tapCount === 0) { - // no failing requirements, immediately trigger the tap event - // or wait as long as the multitap interval to trigger - if (!this.hasRequireFailures()) { - return STATE_RECOGNIZED; - } else { - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.interval, this); - return STATE_BEGAN; - } - } + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; } - return STATE_FAILED; - }, - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + // update the event in the store + store[storeIndex] = ev; - reset: function() { - clearTimeout(this._timer); - }, + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); } } }); - /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * default settings - * @namespace + * Touch events input + * @constructor + * @extends Input */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, - - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, - - /** - * @type {Boolean} - * @default true - */ - enable: true, + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, + Input.apply(this, arguments); + } - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} - */ - preset: [ - // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] - [RotateRecognizer, { enable: false }], - [PinchRecognizer, { enable: false }, ['rotate']], - [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], - [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], - [TapRecognizer], - [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], - [PressRecognizer] - ], + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } - /** - * Some CSS properties can be used to improve the working of Hammer. - * Add them to this method and they will be set when creating a new Manager. - * @namespace - */ - cssProps: { - /** - * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userSelect: 'none', + if (!this.started) { + return; + } - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + var touches = normalizeSingleTouches.call(this, ev, type); - /** - * Disables the default callout shown when you touch and hold a touch target. - * On iOS, when you touch and hold a touch target such as a link, Safari displays - * a callout containing information about the link. This property allows you to disable that callout. - * @type {String} - * @default 'none' - */ - touchCallout: 'none', + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; + } - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); - /** - * Overrides the highlight color shown when the user taps a link or a JavaScript - * clickable element in iOS. This property obeys the alpha value, if specified. - * @type {String} - * @default 'rgba(0,0,0,0)' - */ - tapHighlightColor: 'rgba(0,0,0,0)' + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } + + return [all, changed]; + } + + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL }; - var STOP = 1; - var FORCED_STOP = 2; + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] + * Multi-user touch events input * @constructor + * @extends Input */ - function Manager(element, options) { - options = options || {}; - - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + Input.apply(this, arguments); + } - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; + } - toggleCssProps(this, true); + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); - each(options.recognizers, function(item) { - var recognizer = this.add(new (item[0])(item[1])); - item[2] && recognizer.recognizeWith(item[2]); - item[3] && recognizer.requireFailure(item[3]); - }, this); - } + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function getTouches(ev, type) { + var allTouches = toArray(ev.touches); + var targetIds = this.targetIds; - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + // when there is only one touch, the process can be simplified + if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { + targetIds[allTouches[0].identifier] = true; + return [allTouches, allTouches]; + } - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); - } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); - } - return this; - }, + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - /** - * stop recognizing for this session. - * This session will be discarded, when a new [input]start event is fired. - * When forced, the recognizer cycle is stopped immediately. - * @param {Boolean} [force] - */ - stop: function(force) { - this.session.stopped = force ? FORCED_STOP : STOP; - }, + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); + }); - /** - * run the recognizers! - * called by the inputHandler function on every movement of the pointers (touches) - * it walks through all the recognizers and tries to detect the gesture that is being made - * @param {Object} inputData - */ - recognize: function(inputData) { - var session = this.session; - if (session.stopped) { - return; + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; } + } - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); - - var recognizer; - var recognizers = this.recognizers; - - // this holds the recognizer that is being recognized. - // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED - // if no recognizer is detecting a thing, it is set to `null` - var curRecognizer = session.curRecognizer; + // filter changed touches to only contain touches that exist in the collected target ids + i = 0; + while (i < changedTouches.length) { + if (targetIds[changedTouches[i].identifier]) { + changedTargetTouches.push(changedTouches[i]); + } - // reset when the last recognizer is recognized - // or when we're in a new session - if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { - curRecognizer = session.curRecognizer = null; + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; } + i++; + } - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + if (!changedTargetTouches.length) { + return; + } - // find out if we are allowed try to recognize the input for this one. - // 1. allow if the session is NOT forced stopped (see the .stop() method) - // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one - // that is being recognized. - // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. - // this can be setup with the `recognizeWith()` method on the recognizer. - if (session.stopped !== FORCED_STOP && ( // 1 - !curRecognizer || recognizer == curRecognizer || // 2 - recognizer.canRecognizeWith(curRecognizer))) { // 3 - recognizer.recognize(inputData); - } else { - recognizer.reset(); - } + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the - // current active recognizer. but only if we don't already have an active recognizer - if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { - curRecognizer = session.curRecognizer = recognizer; - } - i++; - } - }, + /** + * Combined touch and mouse input + * + * Touch has a higher priority then mouse, and while touching no mouse events are allowed. + * This because touch devices also emit mouse events while doing a touch. + * + * @constructor + * @extends Input + */ + function TouchMouseInput() { + Input.apply(this, arguments); + + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } + inherit(TouchMouseInput, Input, { /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} + * handle mouse and touch events + * @param {Hammer} manager + * @param {String} inputEvent + * @param {Object} inputData */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; + handler: function TMEhandler(manager, inputEvent, inputData) { + var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), + isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); + + // when we're in a touch event, so block all upcoming mouse events + // most mobile browser also emit mouseevents, right after touchstart + if (isTouch) { + this.mouse.allow = false; + } else if (isMouse && !this.mouse.allow) { + return; } - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; } - return null; + + this.callback(manager, inputEvent, inputData); }, /** - * add a recognizer to the manager - * existing recognizers with the same event name will be removed - * @param {Recognizer} recognizer - * @returns {Recognizer|Manager} + * remove the event listeners */ - add: function(recognizer) { - if (invokeArrayArg(recognizer, 'add', this)) { - return this; - } + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); + } + }); - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - this.recognizers.push(recognizer); - recognizer.manager = this; + // magical touchAction value + var TOUCH_ACTION_COMPUTE = 'compute'; + var TOUCH_ACTION_AUTO = 'auto'; + var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented + var TOUCH_ACTION_NONE = 'none'; + var TOUCH_ACTION_PAN_X = 'pan-x'; + var TOUCH_ACTION_PAN_Y = 'pan-y'; - this.touchAction.update(); - return recognizer; - }, + /** + * Touch Action + * sets the touchAction property or uses the js alternative + * @param {Manager} manager + * @param {String} value + * @constructor + */ + function TouchAction(manager, value) { + this.manager = manager; + this.set(value); + } + TouchAction.prototype = { /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} + * set the touchAction value on the element or enable the polyfill + * @param {String} value */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; + set: function(value) { + // find out the touch-action by the event handlers + if (value == TOUCH_ACTION_COMPUTE) { + value = this.compute(); } - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); - - this.touchAction.update(); - return this; + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + } + this.actions = value.toLowerCase().trim(); }, /** - * bind event - * @param {String} events - * @param {Function} handler - * @returns {EventEmitter} this + * just re-set the touchAction value */ - on: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - handlers[event] = handlers[event] || []; - handlers[event].push(handler); - }); - return this; + update: function() { + this.set(this.manager.options.touchAction); }, /** - * unbind event, leave emit blank to remove all handlers - * @param {String} events - * @param {Function} [handler] - * @returns {EventEmitter} this + * compute the value for the touchAction property based on the recognizer's settings + * @returns {String} value */ - off: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - if (!handler) { - delete handlers[event]; - } else { - handlers[event].splice(inArray(handlers[event], handler), 1); + compute: function() { + var actions = []; + each(this.manager.recognizers, function(recognizer) { + if (boolOrFn(recognizer.options.enable, [recognizer])) { + actions = actions.concat(recognizer.getTouchAction()); } }); - return this; + return cleanTouchActions(actions.join(' ')); }, /** - * emit event to the listeners - * @param {String} event - * @param {Object} data + * this method is called on each input cycle and provides the preventing of the browser behavior + * @param {Object} input */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); + preventDefaults: function(input) { + // not needed with native support for the touchAction property + if (NATIVE_TOUCH_ACTION) { + return; } - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; + + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); return; } - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + var actions = this.actions; + var hasNone = inStr(actions, TOUCH_ACTION_NONE); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); } }, /** - * destroy the manager and unbinds all events - * it doesn't unbind dom events, that is the user own responsibility + * call preventDefault to prevent the browser's default behavior (scrolling in most cases) + * @param {Object} srcEvent */ - destroy: function() { - this.element && toggleCssProps(this, false); - - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; + preventSrc: function(srcEvent) { + this.manager.session.prevented = true; + srcEvent.preventDefault(); } }; /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add - */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } - - /** - * trigger dom event - * @param {String} event - * @param {Object} data + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} */ - function triggerDomEvent(event, data) { - var gestureEvent = document.createEvent('Event'); - gestureEvent.initEvent(event, true, true); - gestureEvent.gesture = data; - data.target.dispatchEvent(gestureEvent); - } - - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, - - STATE_POSSIBLE: STATE_POSSIBLE, - STATE_BEGAN: STATE_BEGAN, - STATE_CHANGED: STATE_CHANGED, - STATE_ENDED: STATE_ENDED, - STATE_RECOGNIZED: STATE_RECOGNIZED, - STATE_CANCELLED: STATE_CANCELLED, - STATE_FAILED: STATE_FAILED, - - DIRECTION_NONE: DIRECTION_NONE, - DIRECTION_LEFT: DIRECTION_LEFT, - DIRECTION_RIGHT: DIRECTION_RIGHT, - DIRECTION_UP: DIRECTION_UP, - DIRECTION_DOWN: DIRECTION_DOWN, - DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, - DIRECTION_VERTICAL: DIRECTION_VERTICAL, - DIRECTION_ALL: DIRECTION_ALL, - - Manager: Manager, - Input: Input, - TouchAction: TouchAction, - - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, - - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, - - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); - - if ("function" == TYPE_FUNCTION && __webpack_require__(11)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } - - })(window, document, 'Hammer'); - - -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; + } - 'use strict'; + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - Object.defineProperty(exports, '__esModule', { - value: true - }); + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } - var util = __webpack_require__(2); - var Hammer = __webpack_require__(31); - var hammerUtil = __webpack_require__(34); + return TOUCH_ACTION_AUTO; + } /** - * clears the toolbar div element of children + * Recognizer flow explained; * + * All recognizers have the initial state of POSSIBLE when a input session starts. + * The definition of a input session is from the first input until the last input, with all it's movement in it. * + * Example session for mouse-input: mousedown -> mousemove -> mouseup * - * @private + * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed + * which determines with state it should be. + * + * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to + * POSSIBLE to give it another change on the next cycle. + * + * Possible + * | + * +-----+---------------+ + * | | + * +-----+-----+ | + * | | | + * Failed Cancelled | + * +-------+------+ + * | | + * Recognized Began + * | + * Changed + * | + * Ended/Recognized */ + var STATE_POSSIBLE = 1; + var STATE_BEGAN = 2; + var STATE_CHANGED = 4; + var STATE_ENDED = 8; + var STATE_RECOGNIZED = STATE_ENDED; + var STATE_CANCELLED = 16; + var STATE_FAILED = 32; - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - var _this = this; + /** + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options + */ + function Recognizer(options) { + this.id = uniqueId(); - _classCallCheck(this, ManipulationSystem); + this.manager = null; + this.options = merge(options || {}, this.defaults); - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - this.editMode = false; - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + this.state = STATE_POSSIBLE; - this.manipulationHammers = []; - this.temporaryUIFunctions = {}; - this.temporaryEventFunctions = []; + this.simultaneous = {}; + this.requireFail = []; + } - this.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.inMode = false; - this.selectedControlNode = undefined; + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - this.options = {}; - this.defaultOptions = { - enabled: false, - initiallyActive: false, - addNode: true, - addEdge: true, - editNode: undefined, - editEdge: true, - deleteNode: true, - deleteEdge: true, - controlNodeStyle: { - shape: 'dot', - size: 6, - color: { background: '#ff0000', border: '#3c3c3c', highlight: { background: '#07f968', border: '#3c3c3c' } }, - borderWidth: 2, - borderWidthSelected: 2 - } - }; - util.extend(this.options, this.defaultOptions); + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - this.body.emitter.on('destroy', function () { - _this._clean(); - }); - this.body.emitter.on('_dataChanged', this._restore.bind(this)); - this.body.emitter.on('_resetData', this._restore.bind(this)); - } + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - _createClass(ManipulationSystem, [{ - key: '_restore', + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; + } + + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); + } + return this; + }, /** - * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. - * @private + * drop the simultaneous link. it doesnt remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this */ - value: function _restore() { - if (this.inMode !== false) { - if (this.options.initiallyActive === true) { - this.enableEditMode(); - } else { - this.disableEditMode(); + dropRecognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { + return this; } - } - } - }, { - key: 'setOptions', + + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, /** - * Set the Options - * @param options + * recognizer can only run when an other is failing + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this */ - value: function setOptions(options, allOptions, globalOptions) { - if (allOptions !== undefined) { - if (allOptions.locale !== undefined) { - this.options.locale = allOptions.locale; - } else { - this.options.locale = globalOptions.locale; + requireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { + return this; } - if (allOptions.locales !== undefined) { - this.options.locales = allOptions.locales; - } else { - this.options.locales = globalOptions.locales; + + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); } - } + return this; + }, - if (options !== undefined) { - if (typeof options === 'boolean') { - this.options.enabled = options; - } else { - this.options.enabled = true; - util.deepExtend(this.options, options); + /** + * drop the requireFailure link. it does not remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRequireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { + return this; } - if (this.options.initiallyActive === true) { - this.editMode = true; + + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); } - this._setup(); - } - } - }, { - key: 'toggleEditMode', + return this; + }, /** - * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. - * - * @private + * has require failures boolean + * @returns {boolean} */ - value: function toggleEditMode() { - if (this.editMode === true) { - this.disableEditMode(); - } else { - this.enableEditMode(); - } - } - }, { - key: 'enableEditMode', - value: function enableEditMode() { - this.editMode = true; - - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'block'; - this.closeDiv.style.display = 'block'; - this.editModeDiv.style.display = 'none'; - this.showManipulatorToolbar(); - } - } - }, { - key: 'disableEditMode', - value: function disableEditMode() { - this.editMode = false; + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'none'; - this.closeDiv.style.display = 'none'; - this.editModeDiv.style.display = 'block'; - this._createEditButton(); - } - } - }, { - key: 'showManipulatorToolbar', + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, /** - * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private + * You should use `tryEmit` instead of `emit` directly to check + * that all the needed recognizers has failed before emitting. + * @param {Object} input */ - value: function showManipulatorToolbar() { - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + emit: function(input) { + var self = this; + var state = this.state; - // reset global letiables - this.manipulationDOM = {}; + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } - // if the gui is enabled, draw all elements. - if (this.guiEnabled === true) { - // a _restore will hide these menus - this.editMode = true; - this.manipulationDiv.style.display = 'block'; - this.closeDiv.style.display = 'block'; + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - var selectedTotalCount = selectedNodeCount + selectedEdgeCount; - var locale = this.options.locales[this.options.locale]; - var needSeperator = false; + emit(); // simple 'eventName' events - if (this.options.addNode !== false) { - this._createAddNodeButton(locale); - needSeperator = true; - } - if (this.options.addEdge !== false) { - if (needSeperator === true) { - this._createSeperator(1); - } else { - needSeperator = true; - } - this._createAddEdgeButton(locale); + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); } + }, - if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { - if (needSeperator === true) { - this._createSeperator(2); - } else { - needSeperator = true; - } - this._createEditNodeButton(locale); - } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { - if (needSeperator === true) { - this._createSeperator(3); - } else { - needSeperator = true; - } - this._createEditEdgeButton(locale); + /** + * Check that all the require failure recognizers has failed, + * if true, it emits a gesture event, + * otherwise, setup the state to FAILED. + * @param {Object} input + */ + tryEmit: function(input) { + if (this.canEmit()) { + return this.emit(input); } + // it's failing anyway + this.state = STATE_FAILED; + }, - // remove buttons - if (selectedTotalCount !== 0) { - if (selectedNodeCount === 1 && this.options.deleteNode !== false) { - if (needSeperator === true) { - this._createSeperator(4); - } - this._createDeleteButton(locale); - } else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { - if (needSeperator === true) { - this._createSeperator(4); + /** + * can we emit? + * @returns {boolean} + */ + canEmit: function() { + var i = 0; + while (i < this.requireFail.length) { + if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { + return false; } - this._createDeleteButton(locale); - } + i++; } + return true; + }, - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + /** + * update the recognizer + * @param {Object} inputData + */ + recognize: function(inputData) { + // make a new copy of the inputData + // so we can change the inputData without messing up the other recognizers + var inputDataClone = extend({}, inputData); - // refresh this bar based on what has been selected - this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); - } + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; + } - // redraw to show any possible changes - this.body.emitter.emit('_redraw'); - } - }, { - key: 'addNodeMode', + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; + } + + this.state = this.process(inputDataClone); + + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, /** - * Create the toolbar for adding Nodes - * - * @private + * return the state of the recognizer + * the actual recognizing happens in this method + * @virtual + * @param {Object} inputData + * @returns {Const} STATE */ - value: function addNodeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + process: function(inputData) { }, // jshint ignore:line - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - this.inMode = 'addNode'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + /** + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; + } + return ''; + } - this._temporaryBindEvent('click', this._performAddNode.bind(this)); + /** + * direction cons to string + * @param {Const} direction + * @returns {String} + */ + function directionStr(direction) { + if (direction == DIRECTION_DOWN) { + return 'down'; + } else if (direction == DIRECTION_UP) { + return 'up'; + } else if (direction == DIRECTION_LEFT) { + return 'left'; + } else if (direction == DIRECTION_RIGHT) { + return 'right'; } - }, { - key: 'editNode', + return ''; + } + + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} + */ + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); + } + return otherRecognizer; + } + + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } + + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, /** - * call the bound function to handle the editing of the node. The node has to be selected. - * - * @private + * Used to check if it the recognizer receives valid input, like input.distance > 10. + * @memberof AttrRecognizer + * @param {Object} input + * @returns {Boolean} recognized */ - value: function editNode() { - var _this2 = this; + attrTest: function(input) { + var optionPointers = this.options.pointers; + return optionPointers === 0 || input.pointers.length === optionPointers; + }, - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + /** + * Process the input and return the state for the recognizer + * @memberof AttrRecognizer + * @param {Object} input + * @returns {*} State + */ + process: function(input) { + var state = this.state; + var eventType = input.eventType; - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - var node = this.selectionHandler._getSelectedNode(); - if (node !== undefined) { - this.inMode = 'editNode'; - if (typeof this.options.editNode === 'function') { - if (node.isCluster !== true) { - var data = util.deepExtend({}, node.options, true); - data.x = node.x; - data.y = node.y; + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - if (this.options.editNode.length === 2) { - this.options.editNode(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'editNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this2.body.data.nodes.getDataSet().update(finalizedData); - } - _this2.showManipulatorToolbar(); - }); - } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); + // on cancel input and we've recognized before, return STATE_CANCELLED + if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { + return state | STATE_CANCELLED; + } else if (isRecognized || isValid) { + if (eventType & INPUT_END) { + return state | STATE_ENDED; + } else if (!(state & STATE_BEGAN)) { + return STATE_BEGAN; } - } else { - alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); - } - } else { - throw new Error('No function has been configured to handle the editing of nodes.'); + return state | STATE_CHANGED; } - } else { - this.showManipulatorToolbar(); - } + return STATE_FAILED; } - }, { - key: 'addEdgeMode', + }); - /** - * create the toolbar to connect nodes - * - * @private - */ - value: function addEdgeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + this.pX = null; + this.pY = null; + } - this.inMode = 'addEdge'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); + } + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); + } + return actions; + }, - // temporarily overload functions - this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); - this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); - this._temporaryBindUI('onDrag', this._dragControlNode.bind(this)); - this._temporaryBindUI('onRelease', this._finishConnect.bind(this)); + directionTest: function(input) { + var options = this.options; + var hasMoved = true; + var distance = input.distance; + var direction = input.direction; + var x = input.deltaX; + var y = input.deltaY; - this._temporaryBindUI('onDragStart', function () {}); - this._temporaryBindUI('onHold', function () {}); - } - }, { - key: 'editEdgeMode', + // lock to axis? + if (!(direction & options.direction)) { + if (options.direction & DIRECTION_HORIZONTAL) { + direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + hasMoved = x != this.pX; + distance = Math.abs(input.deltaX); + } else { + direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; + hasMoved = y != this.pY; + distance = Math.abs(input.deltaY); + } + } + input.direction = direction; - /** - * create the toolbar to edit edges - * - * @private - */ - value: function editEdgeMode() { - var _this3 = this; + return hasMoved && distance > options.threshold && direction & options.direction; + }, - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - this.inMode = 'editEdge'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + this._super.emit.call(this, input); + } + }); - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - if (this.edgeBeingEditedId !== undefined) { - (function () { - var edge = _this3.body.edges[_this3.edgeBeingEditedId]; + /** + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer + */ + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - // create control nodes - var controlNodeFrom = _this3._getNewTargetNode(edge.from.x, edge.from.y); - var controlNodeTo = _this3._getNewTargetNode(edge.to.x, edge.to.y); + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - _this3.temporaryIds.nodes.push(controlNodeFrom.id); - _this3.temporaryIds.nodes.push(controlNodeTo.id); + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - _this3.body.nodes[controlNodeFrom.id] = controlNodeFrom; - _this3.body.nodeIndices.push(controlNodeFrom.id); - _this3.body.nodes[controlNodeTo.id] = controlNodeTo; - _this3.body.nodeIndices.push(controlNodeTo.id); + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI - _this3._temporaryBindUI('onTouch', _this3._controlNodeTouch.bind(_this3)); // used to get the position - _this3._temporaryBindUI('onTap', function () {}); // disabled - _this3._temporaryBindUI('onHold', function () {}); // disabled - _this3._temporaryBindUI('onDragStart', _this3._controlNodeDragStart.bind(_this3)); // used to select control node - _this3._temporaryBindUI('onDrag', _this3._controlNodeDrag.bind(_this3)); // used to drag control node - _this3._temporaryBindUI('onDragEnd', _this3._controlNodeDragEnd.bind(_this3)); // used to connect or revert control nodes - _this3._temporaryBindUI('onMouseMove', function () {}); // disabled + emit: function(input) { + this._super.emit.call(this, input); + if (input.scale !== 1) { + var inOut = input.scale < 1 ? 'in' : 'out'; + this.manager.emit(this.options.event + inOut, input); + } + } + }); - // create function to position control nodes correctly on movement - // automatically cleaned up because we use the temporary bind - _this3._temporaryBindEvent('beforeDrawing', function (ctx) { - var positions = edge.edgeType.findBorderPositions(ctx); - if (controlNodeFrom.selected === false) { - controlNodeFrom.x = positions.from.x; - controlNodeFrom.y = positions.from.y; - } - if (controlNodeTo.selected === false) { - controlNodeTo.x = positions.to.x; - controlNodeTo.y = positions.to.y; - } - }); + /** + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer + */ + function PressRecognizer() { + Recognizer.apply(this, arguments); - _this3.body.emitter.emit('_redraw'); - })(); - } else { - this.showManipulatorToolbar(); - } - } - }, { - key: 'deleteSelected', + this._timer = null; + this._input = null; + } + inherit(PressRecognizer, Recognizer, { /** - * delete everything in the selection - * - * @private + * @namespace + * @memberof PressRecognizer */ - value: function deleteSelected() { - var _this4 = this; + defaults: { + event: 'press', + pointers: 1, + time: 500, // minimal time of the pointer to be pressed + threshold: 5 // a minimal movement is ok, but keep it low + }, - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + process: function(input) { + var options = this.options; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTime = input.deltaTime > options.time; - this.inMode = 'delete'; - var selectedNodes = this.selectionHandler.getSelectedNodes(); - var selectedEdges = this.selectionHandler.getSelectedEdges(); - var deleteFunction = undefined; - if (selectedNodes.length > 0) { - for (var i = 0; i < selectedNodes.length; i++) { - if (this.body.nodes[selectedNodes[i]].isCluster === true) { - alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); - return; - } - } + this._input = input; - if (typeof this.options.deleteNode === 'function') { - deleteFunction = this.options.deleteNode; + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { + this.reset(); + } else if (input.eventType & INPUT_START) { + this.reset(); + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.time, this); + } else if (input.eventType & INPUT_END) { + return STATE_RECOGNIZED; } - } else if (selectedEdges.length > 0) { - if (typeof this.options.deleteEdge === 'function') { - deleteFunction = this.options.deleteEdge; + return STATE_FAILED; + }, + + reset: function() { + clearTimeout(this._timer); + }, + + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - } - if (typeof deleteFunction === 'function') { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length === 2) { - deleteFunction(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'delete') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this4.body.data.edges.getDataSet().remove(finalizedData.edges); - _this4.body.data.nodes.getDataSet().remove(finalizedData.nodes); - _this4.body.emitter.emit('startSimulation'); - _this4.showManipulatorToolbar(); - } else { - _this4.body.emitter.emit('startSimulation'); - _this4.showManipulatorToolbar(); - } - }); + if (input && (input.eventType & INPUT_END)) { + this.manager.emit(this.options.event + 'up', input); } else { - throw new Error('The function for delete does not support two arguments (data, callback)'); + this._input.timeStamp = now(); + this.manager.emit(this.options.event, this._input); } - } else { - this.body.data.edges.getDataSet().remove(selectedEdges); - this.body.data.nodes.getDataSet().remove(selectedNodes); - this.body.emitter.emit('startSimulation'); - this.showManipulatorToolbar(); - } } - }, { - key: '_setup', + }); - //********************************************** PRIVATE ***************************************// + /** + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer + */ + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } + inherit(RotateRecognizer, AttrRecognizer, { /** - * draw or remove the DOM - * @private + * @namespace + * @memberof RotateRecognizer */ - value: function _setup() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - this._createWrappers(); - if (this.editMode === false) { - this._createEditButton(); - } else { - this.showManipulatorToolbar(); - } - } else { - this._removeManipulationDOM(); + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - // disable the gui - this.guiEnabled = false; - } + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); } - }, { - key: '_createWrappers', + }); + + /** + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } + inherit(SwipeRecognizer, AttrRecognizer, { /** - * create the div overlays that contain the DOM - * @private + * @namespace + * @memberof SwipeRecognizer */ - value: function _createWrappers() { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement('div'); - this.manipulationDiv.className = 'vis-manipulation'; - if (this.editMode === true) { - this.manipulationDiv.style.display = 'block'; - } else { - this.manipulationDiv.style.display = 'none'; - } - this.canvas.frame.appendChild(this.manipulationDiv); - } - - // container for the edit button. - if (this.editModeDiv === undefined) { - this.editModeDiv = document.createElement('div'); - this.editModeDiv.className = 'vis-edit-mode'; - if (this.editMode === true) { - this.editModeDiv.style.display = 'none'; - } else { - this.editModeDiv.style.display = 'block'; - } - this.canvas.frame.appendChild(this.editModeDiv); - } + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - // container for the close div button - if (this.closeDiv === undefined) { - this.closeDiv = document.createElement('div'); - this.closeDiv.className = 'vis-close'; - this.closeDiv.style.display = this.manipulationDiv.style.display; - this.canvas.frame.appendChild(this.closeDiv); - } - } - }, { - key: '_getNewTargetNode', + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - /** - * generate a new target node. Used for creating new edges and editing edges - * @param x - * @param y - * @returns {*} - * @private - */ - value: function _getNewTargetNode(x, y) { - var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - controlNodeStyle.id = 'targetNode' + util.randomUUID(); - controlNodeStyle.hidden = false; - controlNodeStyle.physics = false; - controlNodeStyle.x = x; - controlNodeStyle.y = y; + if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { + velocity = input.velocity; + } else if (direction & DIRECTION_HORIZONTAL) { + velocity = input.velocityX; + } else if (direction & DIRECTION_VERTICAL) { + velocity = input.velocityY; + } - return this.body.functions.createNode(controlNodeStyle); - } - }, { - key: '_createEditButton', + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - /** - * Create the edit button - */ - value: function _createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - // reset the manipulationDOM - this.manipulationDOM = {}; + this.manager.emit(this.options.event, input); + } + }); - // empty the editModeDiv - util.recursiveDOMDelete(this.editModeDiv); + /** + * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur + * between the given interval and position. The delay option can be used to recognize multi-taps without firing + * a single tap. + * + * The eventData from the emitted event contains the property `tapCount`, which contains the amount of + * multi-taps being recognized. + * @constructor + * @extends Recognizer + */ + function TapRecognizer() { + Recognizer.apply(this, arguments); - // create the contents for the editMode button - var locale = this.options.locales[this.options.locale]; - var button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']); - this.editModeDiv.appendChild(button); + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - // bind a hammer listener to the button, calling the function toggleEditMode. - this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); - } - }, { - key: '_clean', + this._timer = null; + this._input = null; + this.count = 0; + } + inherit(TapRecognizer, Recognizer, { /** - * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. - * @private + * @namespace + * @memberof PinchRecognizer */ - value: function _clean() { - // not in mode - this.inMode = false; - - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); - - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } + defaults: { + event: 'tap', + pointers: 1, + taps: 1, + interval: 300, // max time between the multi-tap taps + time: 250, // max time of the pointer to be down (like finger on the screen) + threshold: 2, // a minimal movement is ok, but keep it low + posThreshold: 10 // a multi-tap can be a bit off the initial position + }, - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - // restore overloaded UI functions - this._unbindTemporaryUIs(); + process: function(input) { + var options = this.options; - // remove the temporaryEventFunctions - this._unbindTemporaryEvents(); + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - // restore the physics if required - this.body.emitter.emit('restorePhysics'); - } - }, { - key: '_cleanManipulatorHammers', + this.reset(); - /** - * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. - * @private - */ - value: function _cleanManipulatorHammers() { - // _clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].destroy(); + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - this.manipulationHammers = []; - } - } - }, { - key: '_removeManipulationDOM', - /** - * Remove all DOM elements created by this module. - * @private - */ - value: function _removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (validMovement && validTouchTime && validPointers) { + if (input.eventType != INPUT_END) { + return this.failTimeout(); + } - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - // remove the manipulation divs - if (this.manipulationDiv) { - this.canvas.frame.removeChild(this.manipulationDiv); - } - if (this.editModeDiv) { - this.canvas.frame.removeChild(this.editModeDiv); - } - if (this.closeDiv) { - this.canvas.frame.removeChild(this.manipulationDiv); - } + this.pTime = input.timeStamp; + this.pCenter = input.center; - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; - } - }, { - key: '_createSeperator', + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } - /** - * create a seperator line. the index is to differentiate in the manipulation dom - * @param index - * @private - */ - value: function _createSeperator() { - var index = arguments[0] === undefined ? 1 : arguments[0]; + this._input = input; - this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); - } - }, { - key: '_createAddNodeButton', + // if tap count matches we have recognized it, + // else it has began recognizing... + var tapCount = this.count % options.taps; + if (tapCount === 0) { + // no failing requirements, immediately trigger the tap event + // or wait as long as the multitap interval to trigger + if (!this.hasRequireFailures()) { + return STATE_RECOGNIZED; + } else { + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.interval, this); + return STATE_BEGAN; + } + } + } + return STATE_FAILED; + }, - // ---------------------- DOM functions for buttons --------------------------// + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, - value: function _createAddNodeButton(locale) { - var button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addNodeMode.bind(this)); - } - }, { - key: '_createAddEdgeButton', - value: function _createAddEdgeButton(locale) { - var button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); - } - }, { - key: '_createEditNodeButton', - value: function _createEditNodeButton(locale) { - var button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editNode.bind(this)); - } - }, { - key: '_createEditEdgeButton', - value: function _createEditEdgeButton(locale) { - var button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); - } - }, { - key: '_createDeleteButton', - value: function _createDeleteButton(locale) { - var button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.deleteSelected.bind(this)); - } - }, { - key: '_createBackButton', - value: function _createBackButton(locale) { - var button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); - } - }, { - key: '_createButton', - value: function _createButton(id, className, label) { - var labelClassName = arguments[3] === undefined ? 'vis-label' : arguments[3]; + reset: function() { + clearTimeout(this._timer); + }, - this.manipulationDOM[id + 'Div'] = document.createElement('div'); - this.manipulationDOM[id + 'Div'].className = className; - this.manipulationDOM[id + 'Label'] = document.createElement('div'); - this.manipulationDOM[id + 'Label'].className = labelClassName; - this.manipulationDOM[id + 'Label'].innerHTML = label; - this.manipulationDOM[id + 'Div'].appendChild(this.manipulationDOM[id + 'Label']); - return this.manipulationDOM[id + 'Div']; - } - }, { - key: '_createDescription', - value: function _createDescription(label) { - this.manipulationDiv.appendChild(this._createButton('description', 'vis-button vis-none', label)); + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } } - }, { - key: '_temporaryBindEvent', + }); - // -------------------------- End of DOM functions for buttons ------------------------------// + /** + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); + } + + /** + * @const {string} + */ + Hammer.VERSION = '2.0.4'; + /** + * default settings + * @namespace + */ + Hammer.defaults = { /** - * this binds an event until cleanup by the clean functions. - * @param event - * @param newFunction - * @private + * set if DOM events are being triggered. + * But this is slower and unused by simple implementations, so disabled by default. + * @type {Boolean} + * @default false */ - value: function _temporaryBindEvent(event, newFunction) { - this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); - this.body.emitter.on(event, newFunction); - } - }, { - key: '_temporaryBindUI', + domEvents: false, /** - * this overrides an UI function until cleanup by the clean function - * @param UIfunctionName - * @param newFunction - * @private + * The value for the touchAction property/fallback. + * When set to `compute` it will magically set the correct value based on the added recognizers. + * @type {String} + * @default compute */ - value: function _temporaryBindUI(UIfunctionName, newFunction) { - if (this.body.eventListeners[UIfunctionName] !== undefined) { - this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; - this.body.eventListeners[UIfunctionName] = newFunction; - } else { - throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); - } - } - }, { - key: '_unbindTemporaryUIs', + touchAction: TOUCH_ACTION_COMPUTE, /** - * Restore the overridden UI functions to their original state. - * - * @private + * @type {Boolean} + * @default true */ - value: function _unbindTemporaryUIs() { - for (var functionName in this.temporaryUIFunctions) { - if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { - this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; - delete this.temporaryUIFunctions[functionName]; - } - } - this.temporaryUIFunctions = {}; - } - }, { - key: '_unbindTemporaryEvents', + enable: true, /** - * Unbind the events created by _temporaryBindEvent - * @private + * EXPERIMENTAL FEATURE -- can be removed/changed + * Change the parent input target element. + * If Null, then it is being set the to main element. + * @type {Null|EventTarget} + * @default null */ - value: function _unbindTemporaryEvents() { - for (var i = 0; i < this.temporaryEventFunctions.length; i++) { - var eventName = this.temporaryEventFunctions[i].event; - var boundFunction = this.temporaryEventFunctions[i].boundFunction; - this.body.emitter.off(eventName, boundFunction); - } - this.temporaryEventFunctions = []; - } - }, { - key: '_bindHammerToDiv', + inputTarget: null, /** - * Bind an hammer instance to a DOM element. - * @param domElement - * @param funct + * force an input class + * @type {Null|Function} + * @default null */ - value: function _bindHammerToDiv(domElement, boundFunction) { - var hammer = new Hammer(domElement, {}); - hammerUtil.onTouch(hammer, boundFunction); - this.manipulationHammers.push(hammer); - } - }, { - key: '_cleanupTemporaryNodesAndEdges', + inputClass: null, /** - * Neatly clean up temporary edges and nodes - * @private + * Default recognizer setup when calling `Hammer()` + * When creating a new Manager these will be skipped. + * @type {Array} */ - value: function _cleanupTemporaryNodesAndEdges() { - // _clean temporary edges - for (var i = 0; i < this.temporaryIds.edges.length; i++) { - this.body.edges[this.temporaryIds.edges[i]].disconnect(); - delete this.body.edges[this.temporaryIds.edges[i]]; - var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); - if (indexTempEdge !== -1) { - this.body.edgeIndices.splice(indexTempEdge, 1); - } - } + preset: [ + // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] + [RotateRecognizer, { enable: false }], + [PinchRecognizer, { enable: false }, ['rotate']], + [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], + [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], + [TapRecognizer], + [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], + [PressRecognizer] + ], - // _clean temporary nodes - for (var i = 0; i < this.temporaryIds.nodes.length; i++) { - delete this.body.nodes[this.temporaryIds.nodes[i]]; - var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); - if (indexTempNode !== -1) { - this.body.nodeIndices.splice(indexTempNode, 1); - } - } + /** + * Some CSS properties can be used to improve the working of Hammer. + * Add them to this method and they will be set when creating a new Manager. + * @namespace + */ + cssProps: { + /** + * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userSelect: 'none', - this.temporaryIds = { nodes: [], edges: [] }; - } - }, { - key: '_controlNodeTouch', + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @type {String} + * @default 'none' + */ + touchCallout: 'none', - /** - * the touch is used to get the position of the initial click - * @param event - * @private - */ - value: function _controlNodeTouch(event) { - this.selectionHandler.unselectAll(); - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', + + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', + + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' } - }, { - key: '_controlNodeDragStart', + }; - /** - * the drag start is used to mark one of the control nodes as selected. - * @param event - * @private - */ - value: function _controlNodeDragStart(event) { - var pointer = this.lastTouch; - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - var to = this.body.nodes[this.temporaryIds.nodes[1]]; - var edge = this.body.edges[this.edgeBeingEditedId]; - this.selectedControlNode = undefined; + var STOP = 1; + var FORCED_STOP = 2; - var fromSelect = from.isOverlappingWith(pointerObj); - var toSelect = to.isOverlappingWith(pointerObj); + /** + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Manager(element, options) { + options = options || {}; - if (fromSelect === true) { - this.selectedControlNode = from; - edge.edgeType.from = from; - } else if (toSelect === true) { - this.selectedControlNode = to; - edge.edgeType.to = to; - } + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - this.body.emitter.emit('_redraw'); - } - }, { - key: '_controlNodeDrag', + this.handlers = {}; + this.session = {}; + this.recognizers = []; + + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); + + toggleCssProps(this, true); + + each(options.recognizers, function(item) { + var recognizer = this.add(new (item[0])(item[1])); + item[2] && recognizer.recognizeWith(item[2]); + item[3] && recognizer.requireFailure(item[3]); + }, this); + } + Manager.prototype = { /** - * dragging the control nodes or the canvas - * @param event - * @private + * set options + * @param {Object} options + * @returns {Manager} */ - value: function _controlNodeDrag(event) { - this.body.emitter.emit('disablePhysics'); - var pointer = this.body.functions.getPointer(event.center); - var pos = this.canvas.DOMtoCanvas(pointer); + set: function(options) { + extend(this.options, options); - if (this.selectedControlNode !== undefined) { - this.selectedControlNode.x = pos.x; - this.selectedControlNode.y = pos.y; - } else { - // if the drag was not started properly because the click started outside the network div, start it now. - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; - } - this.body.emitter.emit('_redraw'); - } - }, { - key: '_controlNodeDragEnd', + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); + } + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); + } + return this; + }, /** - * connecting or restoring the control nodes. - * @param event - * @private + * stop recognizing for this session. + * This session will be discarded, when a new [input]start event is fired. + * When forced, the recognizer cycle is stopped immediately. + * @param {Boolean} [force] */ - value: function _controlNodeDragEnd(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var edge = this.body.edges[this.edgeBeingEditedId]; + stop: function(force) { + this.session.stopped = force ? FORCED_STOP : STOP; + }, - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (overlappingNodeIds[i] !== this.selectedControlNode.id) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; + /** + * run the recognizers! + * called by the inputHandler function on every movement of the pointers (touches) + * it walks through all the recognizers and tries to detect the gesture that is being made + * @param {Object} inputData + */ + recognize: function(inputData) { + var session = this.session; + if (session.stopped) { + return; } - } - // perform the connection - if (node !== undefined && this.selectedControlNode !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - if (this.selectedControlNode.id === from.id) { - this._performEditEdge(node.id, edge.to.id); - } else { - this._performEditEdge(edge.from.id, node.id); - } - } - } else { - edge.updateEdgeType(); - this.body.emitter.emit('restorePhysics'); - } - this.body.emitter.emit('_redraw'); - } - }, { - key: '_handleConnect', + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + var recognizer; + var recognizers = this.recognizers; - // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - value: function _handleConnect(event) { - // check to avoid double fireing of this function. - if (new Date().valueOf() - this.touchTime > 100) { - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + // this holds the recognizer that is being recognized. + // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED + // if no recognizer is detecting a thing, it is set to `null` + var curRecognizer = session.curRecognizer; - var pointer = this.lastTouch; - var node = this.selectionHandler.getNodeAt(pointer); + // reset when the last recognizer is recognized + // or when we're in a new session + if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { + curRecognizer = session.curRecognizer = null; + } - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - // create a node the temporary line can look at - var targetNode = this._getNewTargetNode(node.x, node.y); - this.body.nodes[targetNode.id] = targetNode; - this.body.nodeIndices.push(targetNode.id); + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - // create a temporary edge - var connectionEdge = this.body.functions.createEdge({ - id: 'connectionEdge' + util.randomUUID(), - from: node.id, - to: targetNode.id, - physics: false, - smooth: { - enabled: true, - type: 'continuous', - roundness: 0.5 - } - }); - this.body.edges[connectionEdge.id] = connectionEdge; - this.body.edgeIndices.push(connectionEdge.id); + // find out if we are allowed try to recognize the input for this one. + // 1. allow if the session is NOT forced stopped (see the .stop() method) + // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one + // that is being recognized. + // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. + // this can be setup with the `recognizeWith()` method on the recognizer. + if (session.stopped !== FORCED_STOP && ( // 1 + !curRecognizer || recognizer == curRecognizer || // 2 + recognizer.canRecognizeWith(curRecognizer))) { // 3 + recognizer.recognize(inputData); + } else { + recognizer.reset(); + } - this.temporaryIds.nodes.push(targetNode.id); - this.temporaryIds.edges.push(connectionEdge.id); - } + // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the + // current active recognizer. but only if we don't already have an active recognizer + if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { + curRecognizer = session.curRecognizer = recognizer; + } + i++; } - this.touchTime = new Date().valueOf(); - } - } - }, { - key: '_dragControlNode', - value: function _dragControlNode(event) { - var pointer = this.body.functions.getPointer(event.center); - if (this.temporaryIds.nodes[0] !== undefined) { - var targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. - targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); - targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); - this.body.emitter.emit('_redraw'); - } else { - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; - } - } - }, { - key: '_finishConnect', + }, /** - * Connect the new edge to the target if one exists, otherwise remove temp line - * @param event - * @private + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} */ - value: function _finishConnect(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - - // remember the edge id - var connectFromId = undefined; - if (this.temporaryIds.edges[0] !== undefined) { - connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; - } + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; + } - // get the overlapping node but NOT the temporary node; - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - // if the node id is NOT a temporary node, accept the node. - if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } } - } + return null; + }, - // clean temporary nodes and edges. - this._cleanupTemporaryNodesAndEdges(); + /** + * add a recognizer to the manager + * existing recognizers with the same event name will be removed + * @param {Recognizer} recognizer + * @returns {Recognizer|Manager} + */ + add: function(recognizer) { + if (invokeArrayArg(recognizer, 'add', this)) { + return this; + } - // perform the connection - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - this._performAddEdge(connectFromId, node.id); - } + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); } - } - this.body.emitter.emit('_redraw'); - } - }, { - key: '_performAddNode', - // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + this.recognizers.push(recognizer); + recognizer.manager = this; - // ------------------------------ Performing all the actual data manipulation ------------------------// + this.touchAction.update(); + return recognizer; + }, /** - * Adds a node on the specified location + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} */ - value: function _performAddNode(clickData) { - var _this5 = this; + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - var defaultData = { - id: util.randomUUID(), - x: clickData.pointer.canvas.x, - y: clickData.pointer.canvas.y, - label: 'new' - }; + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - if (typeof this.options.addNode === 'function') { - if (this.options.addNode.length === 2) { - this.options.addNode(defaultData, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this5.body.data.nodes.getDataSet().add(finalizedData); - _this5.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this.showManipulatorToolbar(); - } - } else { - this.body.data.nodes.getDataSet().add(defaultData); - this.showManipulatorToolbar(); - } - } - }, { - key: '_performAddEdge', + this.touchAction.update(); + return this; + }, /** - * connect two nodes with a new edge. - * - * @private + * bind event + * @param {String} events + * @param {Function} handler + * @returns {EventEmitter} this */ - value: function _performAddEdge(sourceNodeId, targetNodeId) { - var _this6 = this; + on: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + handlers[event] = handlers[event] || []; + handlers[event].push(handler); + }); + return this; + }, - var defaultData = { from: sourceNodeId, to: targetNodeId }; - if (typeof this.options.addEdge === 'function') { - if (this.options.addEdge.length === 2) { - this.options.addEdge(defaultData, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this6.inMode === 'addEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this6.body.data.edges.add(finalizedData); - _this6.selectionHandler.unselectAll(); - _this6.showManipulatorToolbar(); + /** + * unbind event, leave emit blank to remove all handlers + * @param {String} events + * @param {Function} [handler] + * @returns {EventEmitter} this + */ + off: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + if (!handler) { + delete handlers[event]; + } else { + handlers[event].splice(inArray(handlers[event], handler), 1); } - }); - } else { - throw new Error('The function for connect does not support two arguments (data,callback)'); - } - } else { - this.body.data.edges.add(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } - } - }, { - key: '_performEditEdge', + }); + return this; + }, /** - * connect two nodes with a new edge. - * - * @private + * emit event to the listeners + * @param {String} event + * @param {Object} data */ - value: function _performEditEdge(sourceNodeId, targetNodeId) { - var _this7 = this; + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); + } - var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; - if (typeof this.options.editEdge === 'function') { - if (this.options.editEdge.length === 2) { - this.options.editEdge(defaultData, function (finalizedData) { - if (finalizedData === null || finalizedData === undefined || _this7.inMode !== 'editEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this7.body.edges[defaultData.id].updateEdgeType(); - _this7.body.emitter.emit('_redraw'); - } else { - _this7.body.data.edges.update(finalizedData); - _this7.selectionHandler.unselectAll(); - _this7.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; } - } else { - this.body.data.edges.update(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } + + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; + + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; + } + }, + + /** + * destroy the manager and unbinds all events + * it doesn't unbind dom events, that is the user own responsibility + */ + destroy: function() { + this.element && toggleCssProps(this, false); + + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; } - }]); + }; - return ManipulationSystem; - })(); + /** + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add + */ + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); + } + + /** + * trigger dom event + * @param {String} event + * @param {Object} data + */ + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } + + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, + + STATE_POSSIBLE: STATE_POSSIBLE, + STATE_BEGAN: STATE_BEGAN, + STATE_CHANGED: STATE_CHANGED, + STATE_ENDED: STATE_ENDED, + STATE_RECOGNIZED: STATE_RECOGNIZED, + STATE_CANCELLED: STATE_CANCELLED, + STATE_FAILED: STATE_FAILED, + + DIRECTION_NONE: DIRECTION_NONE, + DIRECTION_LEFT: DIRECTION_LEFT, + DIRECTION_RIGHT: DIRECTION_RIGHT, + DIRECTION_UP: DIRECTION_UP, + DIRECTION_DOWN: DIRECTION_DOWN, + DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, + DIRECTION_VERTICAL: DIRECTION_VERTICAL, + DIRECTION_ALL: DIRECTION_ALL, + + Manager: Manager, + Input: Input, + TouchAction: TouchAction, + + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, + + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, + + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); + + if ("function" == TYPE_FUNCTION && __webpack_require__(11)) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { + return Hammer; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof module != 'undefined' && module.exports) { + module.exports = Hammer; + } else { + window[exportName] = Hammer; + } + + })(window, document, 'Hammer'); - exports['default'] = ManipulationSystem; - module.exports = exports['default']; /***/ }, /* 8 */ @@ -16610,7 +16614,7 @@ return /******/ (function(modules) { // webpackBootstrap if (typeof window !== 'undefined') { var propagating = __webpack_require__(32); - var Hammer = window['Hammer'] || __webpack_require__(6); + var Hammer = window['Hammer'] || __webpack_require__(7); module.exports = propagating(Hammer, { preventDefault: 'mouse' }); @@ -29665,7 +29669,7 @@ return /******/ (function(modules) { // webpackBootstrap var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); - var _modulesManipulationSystem = __webpack_require__(7); + var _modulesManipulationSystem = __webpack_require__(5); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); @@ -37744,7 +37748,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNavigationHandler = __webpack_require__(5); + var _componentsNavigationHandler = __webpack_require__(6); var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 0fb2e64d..a6119bde 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -494,6 +494,11 @@ class PhysicsEngine { iterations = this.options.stabilization.iterations; } + if (this.physicsBody.physicsNodeIndices.length === 0) { + return; + } + + // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes");