From f81b41c16b9094b9bf4eb58c3533308e845f4021 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 7 May 2015 15:00:27 +0200 Subject: [PATCH] fixed docs (added tabs) fixed gephiparser, fixed edges, labels on selfreferenced edges, fixed dotlanguage --- dist/vis.js | 25068 ++++++++++++----------- docs/css/newdocs.css | 2 +- docs/js/toggleTable.js | 17 +- docs/network/edges.html | 4 +- docs/network/new_network.html | 1757 +- examples/network/01_basic_usage.html | 3 +- lib/network/dotparser.js | 5 +- lib/network/gephiParser.js | 22 +- lib/network/modules/EdgesHandler.js | 2 +- lib/network/modules/components/Edge.js | 7 +- 10 files changed, 13614 insertions(+), 13273 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index d331b48b..412d2ce8 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -6301,6 +6301,12 @@ return /******/ (function(modules) { // webpackBootstrap var CustomTime = __webpack_require__(27); var ItemSet = __webpack_require__(32); + var ConfigurationSystem = __webpack_require__(45); + var Validator = __webpack_require__(46)['default']; + var printStyle = __webpack_require__(46).printStyle; + var allOptions = __webpack_require__(47).allOptions; + var configureOptions = __webpack_require__(47).configureOptions; + /** * Create a timeline visualization * @param {HTMLElement} container @@ -6402,6 +6408,9 @@ return /******/ (function(modules) { // webpackBootstrap me.emit('contextmenu', me.getEventProperties(event)); }; + // setup configuration system + this.configurationSystem = new ConfigurationSystem(this, container, configureOptions); + // apply options if (options) { this.setOptions(options); @@ -6433,6 +6442,30 @@ return /******/ (function(modules) { // webpackBootstrap this._redraw(); }; + Timeline.prototype.setOptions = function (options) { + // validate options + var errorFound = Validator.validate(options, allOptions); + if (errorFound === true) { + options = {}; + console.log('%cErrors have been found in the supplied options object. None of the options will be used.', printStyle); + } + + Core.prototype.setOptions.call(this, options); + + if ('type' in options) { + if (options.type !== this.options.type) { + this.options.type = options.type; + + // force recreation of all items + var itemsData = this.itemsData; + if (itemsData) { + this.setItems(null); // remove all + this.setItems(itemsData); // add all + } + } + } + }; + /** * Set items * @param {vis.DataSet | Array | null} items @@ -6602,9 +6635,9 @@ return /******/ (function(modules) { // webpackBootstrap */ Timeline.prototype.getItemRange = function () { // calculate min from start filed - var dataset = this.itemsData.getDataSet(), - min = null, - max = null; + var dataset = this.itemsData && this.itemsData.getDataSet(); + var min = null; + var max = null; if (dataset) { // calculate the minimum value of the field 'start' @@ -7695,7 +7728,7 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(45); + var hammerUtil = __webpack_require__(48); var moment = __webpack_require__(40); var Component = __webpack_require__(25); var DateUtil = __webpack_require__(15); @@ -8576,7 +8609,7 @@ return /******/ (function(modules) { // webpackBootstrap /** * Set custom formatting for the minor an major labels of the TimeStep. * Both `minorLabels` and `majorLabels` are an Object with properties: - * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. * @param {{minorLabels: Object, majorLabels: Object}} format */ TimeStep.prototype.setFormat = function (format) { @@ -8775,7 +8808,7 @@ return /******/ (function(modules) { // webpackBootstrap * @param {{scale: string, step: number}} params * An object containing two properties: * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * 'minute', 'hour', 'weekday', 'day', 'month', 'year'. * - A number 'step'. A step size, by default 1. * Choose for example 1, 2, 5, or 10. */ @@ -8910,7 +8943,7 @@ return /******/ (function(modules) { // webpackBootstrap * Static function * @param {Date} date the date to be snapped. * @param {string} scale Current scale, can be 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * 'minute', 'hour', 'weekday, 'day', 'month', 'year'. * @param {number} step Current step (1, 2, 4, 5, ... * @return {Date} snappedDate */ @@ -9602,7 +9635,7 @@ return /******/ (function(modules) { // webpackBootstrap * @Override */ BackgroundItem.prototype.repositionY = function (margin) { - var onTop = this.options.orientation === 'top'; + var onTop = this.options.orientation.item === 'top'; this.dom.content.style.top = onTop ? '' : '0'; this.dom.content.style.bottom = onTop ? '0' : ''; var height; @@ -9869,7 +9902,7 @@ return /******/ (function(modules) { // webpackBootstrap * @Override */ BoxItem.prototype.repositionY = function () { - var orientation = this.options.orientation; + var orientation = this.options.orientation.item; var box = this.dom.box; var line = this.dom.line; var dot = this.dom.dot; @@ -10068,8 +10101,8 @@ return /******/ (function(modules) { // webpackBootstrap * @Override */ PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - point = this.dom.point; + var orientation = this.options.orientation.item; + var point = this.dom.point; if (orientation == 'top') { point.style.top = this.top + 'px'; @@ -10318,8 +10351,8 @@ return /******/ (function(modules) { // webpackBootstrap * @Override */ RangeItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - box = this.dom.box; + var orientation = this.options.orientation.item; + var box = this.dom.box; if (orientation == 'top') { box.style.top = this.top + 'px'; @@ -10443,7 +10476,7 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var Component = __webpack_require__(25); var moment = __webpack_require__(40); - var locales = __webpack_require__(46); + var locales = __webpack_require__(49); /** * A current time bar @@ -10620,7 +10653,7 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var Component = __webpack_require__(25); var moment = __webpack_require__(40); - var locales = __webpack_require__(46); + var locales = __webpack_require__(49); /** * A custom time bar @@ -11461,9 +11494,9 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var DOMutil = __webpack_require__(2); - var Line = __webpack_require__(47); - var Bar = __webpack_require__(48); - var Points = __webpack_require__(49); + var Line = __webpack_require__(50); + var Bar = __webpack_require__(51); + var Points = __webpack_require__(52); /** * /** @@ -12333,7 +12366,9 @@ return /******/ (function(modules) { // webpackBootstrap this.defaultOptions = { type: null, // 'box', 'point', 'range', 'background' - orientation: 'bottom', // item orientation: 'top' or 'bottom' + orientation: { + item: 'bottom' // item orientation: 'top' or 'bottom' + }, align: 'auto', // alignment of box items stack: true, groupOrder: null, @@ -12526,7 +12561,7 @@ return /******/ (function(modules) { // webpackBootstrap * Alignment for the items, only applicable for * BoxItem. Choose 'center' (default), 'left', or * 'right'. - * {String} orientation + * {String} orientation.item * Orientation of the item set. Choose 'top' or * 'bottom' (default). * {Function} groupOrder @@ -12586,9 +12621,9 @@ return /******/ (function(modules) { // webpackBootstrap if ('orientation' in options) { if (typeof options.orientation === 'string') { - this.options.orientation = options.orientation; + this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom'; } else if (typeof options.orientation === 'object' && 'item' in options.orientation) { - this.options.orientation = options.orientation.item; + this.options.orientation.item = options.orientation.item; } } @@ -12805,7 +12840,7 @@ return /******/ (function(modules) { // webpackBootstrap range = this.body.range, asSize = util.option.asSize, options = this.options, - orientation = options.orientation, + orientation = options.orientation.item, resized = false, frame = this.dom.frame, editable = options.editable.updateTime || options.editable.updateGroup; @@ -12877,7 +12912,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ ItemSet.prototype._firstGroup = function () { - var firstGroupIndex = this.options.orientation == 'top' ? 0 : this.groupIds.length - 1; + var firstGroupIndex = this.options.orientation.item == 'top' ? 0 : this.groupIds.length - 1; var firstGroupId = this.groupIds[firstGroupIndex]; var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; @@ -13864,7 +13899,7 @@ return /******/ (function(modules) { // webpackBootstrap return group; } - if (this.options.orientation === 'top') { + if (this.options.orientation.item === 'top') { if (i === this.groupIds.length - 1 && pageY > top) { return group; } @@ -14126,8 +14161,8 @@ return /******/ (function(modules) { // webpackBootstrap var DataAxis = __webpack_require__(28); var GraphGroup = __webpack_require__(29); var Legend = __webpack_require__(33); - var BarFunctions = __webpack_require__(48); - var LineFunctions = __webpack_require__(47); + var BarFunctions = __webpack_require__(51); + var LineFunctions = __webpack_require__(50); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -15126,10 +15161,12 @@ return /******/ (function(modules) { // webpackBootstrap }; this.defaultOptions = { - orientation: 'bottom', // axis orientation: 'top' or 'bottom' + orientation: { + axis: 'bottom' + }, // axis orientation: 'top' or 'bottom' showMinorLabels: true, showMajorLabels: true, - format: null, + format: TimeStep.FORMAT, timeAxis: null }; this.options = util.extend({}, this.defaultOptions); @@ -15148,20 +15185,23 @@ return /******/ (function(modules) { // webpackBootstrap * Set options for the TimeAxis. * Parameters will be merged in current options. * @param {Object} options Available options: - * {string} [orientation] + * {string} [orientation.axis] * {boolean} [showMinorLabels] * {boolean} [showMajorLabels] */ TimeAxis.prototype.setOptions = function (options) { if (options) { // copy all options that we know - util.selectiveExtend(['showMinorLabels', 'showMajorLabels', 'hiddenDates', 'format', 'timeAxis'], this.options, options); + util.selectiveExtend(['showMinorLabels', 'showMajorLabels', 'hiddenDates', 'timeAxis'], this.options, options); + + // deep copy the format options + util.selectiveDeepExtend(['format'], this.options, options); if ('orientation' in options) { if (typeof options.orientation === 'string') { - this.options.orientation = options.orientation; + this.options.orientation.axis = options.orientation; } else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { - this.options.orientation = options.orientation.axis; + this.options.orientation.axis = options.orientation.axis; } } @@ -15215,7 +15255,7 @@ return /******/ (function(modules) { // webpackBootstrap var background = this.dom.background; // determine the correct parent DOM element (depending on option orientation) - var parent = options.orientation == 'top' ? this.body.dom.top : this.body.dom.bottom; + var parent = options.orientation.axis == 'top' ? this.body.dom.top : this.body.dom.bottom; var parentChanged = foreground.parentNode !== parent; // calculate character width and height @@ -15231,7 +15271,7 @@ return /******/ (function(modules) { // webpackBootstrap props.height = props.minorLabelHeight + props.majorLabelHeight; props.width = foreground.offsetWidth; - props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); + props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); props.minorLineWidth = 1; // TODO: really calculate width props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight; props.majorLineWidth = 1; // TODO: really calculate width @@ -15266,7 +15306,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ TimeAxis.prototype._repaintLabels = function () { - var orientation = this.options.orientation; + var orientation = this.options.orientation.axis; // calculate range and step (step such that we have space for 7 characters per label) var start = util.convert(this.body.range.start, 'Number'); @@ -15527,66 +15567,66 @@ return /******/ (function(modules) { // webpackBootstrap function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - var _modulesGroups = __webpack_require__(50); + var _modulesGroups = __webpack_require__(53); var _modulesGroups2 = _interopRequireDefault(_modulesGroups); - var _modulesNodesHandler = __webpack_require__(51); + var _modulesNodesHandler = __webpack_require__(54); var _modulesNodesHandler2 = _interopRequireDefault(_modulesNodesHandler); - var _modulesEdgesHandler = __webpack_require__(52); + var _modulesEdgesHandler = __webpack_require__(55); var _modulesEdgesHandler2 = _interopRequireDefault(_modulesEdgesHandler); - var _modulesPhysicsEngine = __webpack_require__(53); + var _modulesPhysicsEngine = __webpack_require__(56); var _modulesPhysicsEngine2 = _interopRequireDefault(_modulesPhysicsEngine); - var _modulesClustering = __webpack_require__(54); + var _modulesClustering = __webpack_require__(57); var _modulesClustering2 = _interopRequireDefault(_modulesClustering); - var _modulesCanvasRenderer = __webpack_require__(55); + var _modulesCanvasRenderer = __webpack_require__(58); var _modulesCanvasRenderer2 = _interopRequireDefault(_modulesCanvasRenderer); - var _modulesCanvas = __webpack_require__(56); + var _modulesCanvas = __webpack_require__(59); var _modulesCanvas2 = _interopRequireDefault(_modulesCanvas); - var _modulesView = __webpack_require__(57); + var _modulesView = __webpack_require__(60); var _modulesView2 = _interopRequireDefault(_modulesView); - var _modulesInteractionHandler = __webpack_require__(58); + var _modulesInteractionHandler = __webpack_require__(61); var _modulesInteractionHandler2 = _interopRequireDefault(_modulesInteractionHandler); - var _modulesSelectionHandler = __webpack_require__(59); + var _modulesSelectionHandler = __webpack_require__(62); var _modulesSelectionHandler2 = _interopRequireDefault(_modulesSelectionHandler); - var _modulesLayoutEngine = __webpack_require__(60); + var _modulesLayoutEngine = __webpack_require__(63); var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); - var _modulesManipulationSystem = __webpack_require__(61); + var _modulesManipulationSystem = __webpack_require__(64); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); - var _modulesConfigurationSystem = __webpack_require__(62); + var _modulesConfigurationSystem = __webpack_require__(45); var _modulesConfigurationSystem2 = _interopRequireDefault(_modulesConfigurationSystem); - var _modulesValidator = __webpack_require__(63); + var _modulesValidator = __webpack_require__(46); var _modulesValidator2 = _interopRequireDefault(_modulesValidator); - var _modulesComponentsAllOptionsJs = __webpack_require__(64); + var _modulesComponentsAllOptionsJs = __webpack_require__(65); // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(65); + __webpack_require__(66); var Emitter = __webpack_require__(43); var Hammer = __webpack_require__(41); @@ -15596,8 +15636,8 @@ return /******/ (function(modules) { // webpackBootstrap var dotparser = __webpack_require__(38); var gephiParser = __webpack_require__(39); var Images = __webpack_require__(37); - var Activator = __webpack_require__(66); - var locales = __webpack_require__(67); + var Activator = __webpack_require__(67); + var locales = __webpack_require__(68); /** * @constructor Network @@ -16946,7 +16986,8 @@ return /******/ (function(modules) { // webpackBootstrap to: dotEdge.to }; merge(graphEdge, dotEdge.attr); - graphEdge.style = dotEdge.type === '->' ? 'arrow' : 'line'; + graphEdge.arrows = dotEdge.type === '->' ? 'to' : undefined; + return graphEdge; }; @@ -17008,23 +17049,23 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - function parseGephi(gephiJSON, options) { + function parseGephi(gephiJSON, optionsObj) { var edges = []; var nodes = []; - this.options = { + var options = { edges: { inheritColor: true }, nodes: { - allowedToMove: false, + fixed: false, parseColor: false } }; if (options !== undefined) { - this.options.nodes['allowedToMove'] = options.allowedToMove | false; - this.options.nodes['parseColor'] = options.parseColor | false; - this.options.edges['inheritColor'] = options.inheritColor | true; + options.nodes['fixed'] = optionsObj.fixed !== undefined ? options.fixed : false; + options.nodes['parseColor'] = optionsObj.parseColor !== undefined ? options.parseColor : false; + options.edges['inheritColor'] = optionsObj.inheritColor !== undefined ? options.inheritColor : true; } var gEdges = gephiJSON.edges; @@ -17039,7 +17080,7 @@ return /******/ (function(modules) { // webpackBootstrap // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; edge['color'] = gEdge.color; - edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor; + edge['inheritColor'] = edge['color'] !== undefined ? false : options.inheritColor; edges.push(edge); } @@ -17048,17 +17089,17 @@ return /******/ (function(modules) { // webpackBootstrap var gNode = gNodes[i]; node['id'] = gNode.id; node['attributes'] = gNode.attributes; + node['title'] = gNode.title; node['x'] = gNode.x; node['y'] = gNode.y; node['label'] = gNode.label; - if (this.options.nodes.parseColor === true) { + if (options.nodes.parseColor === true) { node['color'] = gNode.color; } else { node['color'] = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; } - node['radius'] = gNode.size; - node['allowedToMoveX'] = this.options.nodes.allowedToMove; - node['allowedToMoveY'] = this.options.nodes.allowedToMove; + node['size'] = gNode.size; + node['fixed'] = options.nodes.fixed && gNode.x !== undefined && gNode.y !== undefined; nodes.push(node); } @@ -17075,7 +17116,7 @@ return /******/ (function(modules) { // webpackBootstrap // use this instance. Else, load via commonjs. 'use strict'; - module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(68); + module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(69); /***/ }, /* 41 */ @@ -17086,8 +17127,8 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; if (typeof window !== 'undefined') { - var propagating = __webpack_require__(69); - var Hammer = window['Hammer'] || __webpack_require__(70); + var propagating = __webpack_require__(70); + var Hammer = window['Hammer'] || __webpack_require__(71); module.exports = propagating(Hammer, { preventDefault: 'mouse' }); @@ -17491,22 +17532,19 @@ return /******/ (function(modules) { // webpackBootstrap var Emitter = __webpack_require__(43); var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(45); + var hammerUtil = __webpack_require__(48); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); var Range = __webpack_require__(17); var ItemSet = __webpack_require__(32); var TimeAxis = __webpack_require__(35); - var Activator = __webpack_require__(66); + var Activator = __webpack_require__(67); var DateUtil = __webpack_require__(15); var CustomTime = __webpack_require__(27); /** * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array} [items] - * @param {Object} [options] See Core.setOptions for the available options. * @constructor */ function Core() {} @@ -17770,6 +17808,18 @@ return /******/ (function(modules) { // webpackBootstrap return component.setOptions(options); }); + // enable/disable configure + if (this.configurationSystem) { + this.configurationSystem.setOptions(options.configure); + + // collect the settings of all components, and pass them to the configuration system + var appliedOptions = util.deepExtend({}, this.options); + this.components.forEach(function (component) { + util.deepExtend(appliedOptions, component.options); + }); + this.configurationSystem.setModuleOptions({ global: appliedOptions }); + } + // redraw everything this._redraw(); }; @@ -18446,2297 +18496,1834 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Hammer = __webpack_require__(41); - - /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst && !isTouching) { - callback(event); - - isTouching = true; - setTimeout(function () { - isTouching = false; - }, 0); - } - }; + Object.defineProperty(exports, '__esModule', { + value: true + }); - hammer.on('hammer.input', callback.inputHandler); - }; + 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; }; })(); - // isTouching is true while a touch action is being emitted - // this is a hack to prevent `touch` from being fired twice - var isTouching = false; + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal && !isReleasing) { - callback(event); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - isReleasing = true; - setTimeout(function () { - isReleasing = false; - }, 0); - } - }; + var _componentsColorPicker = __webpack_require__(72); - return hammer.on('hammer.input', callback.inputHandler); - }; + var _componentsColorPicker2 = _interopRequireDefault(_componentsColorPicker); - // isReleasing is true while a release action is being emitted - // this is a hack to prevent `release` from being fired twice - var isReleasing = false; + var util = __webpack_require__(1); /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. + * Boolean options are recognised as Boolean + * Number options should be written as array: [default value, min value, max value, stepsize] + * Colors should be written as array: ['color', '#ffffff'] + * Strings with should be written as array: [option1, option2, option3, ..] + * + * The options are matched with their counterparts in each of the modules and the values used in the configuration are + * + * @param parentModule | the location where parentModule.setOptions() can be called + * @param defaultContainer | the default container of the module + * @param configureOptions | the fully configured and predefined options set found in allOptions.js + * @param pixelRatio | canvas pixel ratio */ - exports.offTouch = function (hammer, callback) { - hammer.off('hammer.input', callback.inputHandler); - }; - /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.offRelease = exports.offTouch; + var ConfigurationSystem = (function () { + function ConfigurationSystem(parentModule, defaultContainer, configureOptions) { + var pixelRatio = arguments[3] === undefined ? 1 : arguments[3]; -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { + _classCallCheck(this, ConfigurationSystem); - // English - 'use strict'; + this.parent = parentModule; + this.changedOptions = []; + this.container = defaultContainer; - exports['en'] = { - current: 'current', - time: 'time' - }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + this.options = {}; + this.defaultOptions = { + enabled: false, + filter: true, + container: undefined + }; + util.extend(this.options, this.defaultOptions); - // Dutch - exports['nl'] = { - current: 'huidige', - time: 'tijd' - }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; + this.configureOptions = configureOptions; + this.moduleOptions = {}; + this.domElements = []; + this.colorPicker = new _componentsColorPicker2['default'](pixelRatio); + this.wrapper; + } -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { + _createClass(ConfigurationSystem, [{ + key: 'setOptions', - 'use strict'; + /** + * refresh all options. + * Because all modules parse their options by themselves, we just use their options. We copy them here. + * + * @param options + */ + value: function setOptions(options) { + if (options !== undefined) { + var enabled = true; + if (typeof options === 'string') { + this.options.filter = options; + } else if (options instanceof Array) { + this.options.filter = options.join(); + } else if (typeof options === 'object') { + if (options.container !== undefined) { + this.options.container = options.container; + } + if (options.filter !== undefined) { + this.options.filter = options.filter; + } + if (options.enabled !== undefined) { + enabled = options.enabled; + } + } else if (typeof options === 'boolean') { + this.options.filter = true; + enabled = options; + } + this.options.enabled = enabled; + } + this._clean(); + } + }, { + key: 'setModuleOptions', + value: function setModuleOptions(moduleOptions) { + this.moduleOptions = moduleOptions; + if (this.options.enabled === true) { + this._clean(); + if (this.options.container !== undefined) { + this.container = this.options.container; + } + this._create(this.options.filter); + } + } + }, { + key: '_create', - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(49); + /** + * Create all DOM elements + * @param {Boolean | String} config + * @private + */ + value: function _create(config) { + var _this = this; - function Line(groupId, options) { - this.groupId = groupId; - this.options = options; - } + this._clean(); + this.changedOptions = []; - Line.prototype.getData = function (groupData) { - var combinedData = []; - for (var j = 0; j < groupData.length; j++) { - combinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: this.groupId - }); - } - return combinedData; - }; + var counter = 0; + for (var option in this.configureOptions) { + if (this.configureOptions.hasOwnProperty(option)) { + if (config === true || config.indexOf(option) !== -1) { + var optionObj = this.configureOptions[option]; - Line.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + // linebreak between categories + if (counter > 0) { + this._makeItem([]); + } + // a header for the category + this._makeHeader(option); - Line.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { - if (combinedData.length > 0) { - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; + // get the suboptions + var path = [option]; + this._handleObject(optionObj, path); + } + counter++; + } } - }); - var intersections = {}; - - Line._getDataIntersections(intersections, combinedData); - groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; + var generateButton = document.createElement('div'); + generateButton.className = 'vis-network-configuration button'; + generateButton.innerHTML = 'generate options'; + generateButton.onclick = function () { + _this._printOptions(); + }; + generateButton.onmouseover = function () { + generateButton.className = 'vis-network-configuration button hover'; + }; + generateButton.onmouseout = function () { + generateButton.className = 'vis-network-configuration button'; + }; - Line._getStackedYRange = function (intersections, combinedData) { - var key; - var yMin = combinedData[0].y; - var yMax = combinedData[0].y; - for (var i = 0; i < combinedData.length; i++) { - key = combinedData[i].x; - if (intersections[key] === undefined) { - yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; - yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; - } else { - if (combinedData[i].y < 0) { - intersections[key].accumulatedNegative += combinedData[i].y; - } else { - intersections[key].accumulatedPositive += combinedData[i].y; - } - } - } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; - yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; - yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; - yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; - } - } + this.optionsContainer = document.createElement('div'); + this.optionsContainer.className = 'vis-network-configuration vis-option-container'; - return { min: yMin, max: yMax }; - }; + this.domElements.push(this.optionsContainer); + this.domElements.push(generateButton); - /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private - */ - Line._getDataIntersections = function (intersections, combinedData) { - // get intersections - var coreDistance; - for (var i = 0; i < combinedData.length; i++) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); + this._push(); + this.colorPicker.insertTo(this.container); } - if (coreDistance === 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; + }, { + key: '_push', + + /** + * draw all DOM elements on the screen + * @private + */ + value: function _push() { + this.wrapper = document.createElement('div'); + this.wrapper.className = 'vis-network-configuration-wrapper'; + this.container.appendChild(this.wrapper); + for (var i = 0; i < this.domElements.length; i++) { + this.wrapper.appendChild(this.domElements[i]); } - intersections[combinedData[i].x].amount += 1; } - } - }; + }, { + key: '_clean', - /** - * draw a line graph - * - * @param dataset - * @param group - */ - Line.prototype.draw = function (dataset, group, framework) { - if (dataset != null) { - if (dataset.length > 0) { - var path, d; - var svgHeight = Number(framework.svg.style.height.replace('px', '')); - path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); - path.setAttributeNS(null, 'class', group.className); - if (group.style !== undefined) { - path.setAttributeNS(null, 'style', group.style); + /** + * delete all DOM elements + * @private + */ + value: function _clean() { + for (var i = 0; i < this.domElements.length; i++) { + this.wrapper.removeChild(this.domElements[i]); } - // construct path from dataset - if (group.options.catmullRom.enabled == true) { - d = Line._catmullRom(dataset, group); - } else { - d = Line._linear(dataset); + if (this.wrapper !== undefined) { + this.container.removeChild(this.wrapper); + this.wrapper = undefined; } + this.domElements = []; + } + }, { + key: '_getValue', - // append with points for fill and finalize the path - if (group.options.shaded.enabled == true) { - var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); - var dFill; - if (group.options.shaded.orientation == 'top') { - dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0; + /** + * get the value from the actualOptions if it exists + * @param {array} path | where to look for the actual option + * @returns {*} + * @private + */ + value: function _getValue(path) { + var base = this.moduleOptions; + for (var i = 0; i < path.length; i++) { + if (base[path[i]] !== undefined) { + base = base[path[i]]; } else { - dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight; - } - fillPath.setAttributeNS(null, 'class', group.className + ' vis-fill'); - if (group.options.shaded.style !== undefined) { - fillPath.setAttributeNS(null, 'style', group.options.shaded.style); + base = undefined; + break; } - fillPath.setAttributeNS(null, 'd', dFill); } - // copy properties to path for drawing. - path.setAttributeNS(null, 'd', 'M' + d); + return base; + } + }, { + key: '_makeItem', - // draw points - if (group.options.drawPoints.enabled == true) { - Points.draw(dataset, group, framework); + /** + * all option elements are wrapped in an item + * @param path + * @param domElements + * @private + */ + value: function _makeItem(path) { + for (var _len = arguments.length, domElements = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + domElements[_key - 1] = arguments[_key]; } - } - } - }; - /** - * This uses an uniform parametrization of the CatmullRom algorithm: - * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. - * @param data - * @returns {string} - * @private - */ - Line._catmullRomUniform = function (data) { - // catmull rom - var p0, p1, p2, p3, bp1, bp2; - var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; - var normalization = 1 / 6; - var length = data.length; - for (var i = 0; i < length - 1; i++) { + var item = document.createElement('div'); + item.className = 'vis-network-configuration item s' + path.length; + domElements.forEach(function (element) { + item.appendChild(element); + }); + this.domElements.push(item); + } + }, { + key: '_makeHeader', - p0 = i == 0 ? data[0] : data[i - 1]; - p1 = data[i]; - p2 = data[i + 1]; - p3 = i + 2 < length ? data[i + 2] : p2; + /** + * header for major subjects + * @param name + * @private + */ + value: function _makeHeader(name) { + var div = document.createElement('div'); + div.className = 'vis-network-configuration header'; + div.innerHTML = name; + this._makeItem([], div); + } + }, { + key: '_makeLabel', - // Catmull-Rom to Cubic Bezier conversion matrix - // 0 1 0 0 - // -1/6 1 1/6 0 - // 0 1/6 1 -1/6 - // 0 0 1 0 + /** + * make a label, if it is an object label, it gets different styling. + * @param name + * @param path + * @param objectLabel + * @returns {HTMLElement} + * @private + */ + value: function _makeLabel(name, path) { + var objectLabel = arguments[2] === undefined ? false : arguments[2]; - // bp0 = { x: p1.x, y: p1.y }; - bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; - bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; - // bp0 = { x: p2.x, y: p2.y }; + var div = document.createElement('div'); + div.className = 'vis-network-configuration label s' + path.length; + if (objectLabel === true) { + div.innerHTML = '' + name + ':'; + } else { + div.innerHTML = name + ':'; + } + return div; + } + }, { + key: '_makeDropdown', - d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; - } + /** + * make a dropdown list for multiple possible string optoins + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeDropdown(arr, value, path) { + var select = document.createElement('select'); + select.className = 'vis-network-configuration select'; + var selectedValue = 0; + if (value !== undefined) { + if (arr.indexOf(value) !== -1) { + selectedValue = arr.indexOf(value); + } + } - return d; - }; + for (var i = 0; i < arr.length; i++) { + var option = document.createElement('option'); + option.value = arr[i]; + if (i === selectedValue) { + option.selected = 'selected'; + } + option.innerHTML = arr[i]; + select.appendChild(option); + } - /** - * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. - * By default, the centripetal parameterization is used because this gives the nicest results. - * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. - * - * One optimization can be used to reuse distances since this is a sliding window approach. - * @param data - * @param group - * @returns {string} - * @private - */ - Line._catmullRom = function (data, group) { - var alpha = group.options.catmullRom.alpha; - if (alpha == 0 || alpha === undefined) { - return this._catmullRomUniform(data); - } else { - var p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M; - var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; - var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; - var length = data.length; - for (var i = 0; i < length - 1; i++) { + var me = this; + select.onchange = function () { + me._update(this.value, path); + }; - p0 = i == 0 ? data[0] : data[i - 1]; - p1 = data[i]; - p2 = data[i + 1]; - p3 = i + 2 < length ? data[i + 2] : p2; + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, select); + } + }, { + key: '_makeRange', - d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); - d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); - d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); + /** + * make a range object for numeric options + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeRange(arr, value, path) { + var defaultValue = arr[0]; + var min = arr[1]; + var max = arr[2]; + var step = arr[3]; + var range = document.createElement('input'); + range.type = 'range'; + range.className = 'vis-network-configuration range'; + range.min = min; + range.max = max; + range.step = step; - // Catmull-Rom to Cubic Bezier conversion matrix + if (value !== undefined) { + if (value * 0.1 < min) { + range.min = value / 10; + } + if (value * 2 > max && max !== 1) { + range.max = value * 2; + } + range.value = value; + } else { + range.value = defaultValue; + } - // A = 2d1^2a + 3d1^a * d2^a + d3^2a - // B = 2d3^2a + 3d3^a * d2^a + d2^2a + var input = document.createElement('input'); + input.className = 'vis-network-configuration rangeinput'; + input.value = range.value; - // [ 0 1 0 0 ] - // [ -d2^2a /N A/N d1^2a /N 0 ] - // [ 0 d3^2a /M B/M -d2^2a /M ] - // [ 0 0 1 0 ] + var me = this; + range.onchange = function () { + input.value = this.value;me._update(Number(this.value), path); + }; + range.oninput = function () { + input.value = this.value; + }; - d3powA = Math.pow(d3, alpha); - d3pow2A = Math.pow(d3, 2 * alpha); - d2powA = Math.pow(d2, alpha); - d2pow2A = Math.pow(d2, 2 * alpha); - d1powA = Math.pow(d1, alpha); - d1pow2A = Math.pow(d1, 2 * alpha); + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, range, input); + } + }, { + key: '_makeCheckbox', - A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; - B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; - N = 3 * d1powA * (d1powA + d2powA); - if (N > 0) { - N = 1 / N; - } - M = 3 * d3powA * (d3powA + d2powA); - if (M > 0) { - M = 1 / M; + /** + * make a checkbox for boolean options. + * @param defaultValue + * @param value + * @param path + * @private + */ + value: function _makeCheckbox(defaultValue, value, path) { + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'vis-network-configuration checkbox'; + checkbox.checked = defaultValue; + if (value !== undefined) { + checkbox.checked = value; + if (value !== defaultValue) { + if (typeof defaultValue === 'object') { + if (value !== defaultValue.enabled) { + this.changedOptions.push({ path: path, value: value }); + } + } else { + this.changedOptions.push({ path: path, value: value }); + } + } } - bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, - y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + var me = this; + checkbox.onchange = function () { + me._update(this.checked, path); + }; - bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, - y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, checkbox); + } + }, { + key: '_makeTextInput', - if (bp1.x == 0 && bp1.y == 0) { - bp1 = p1; - } - if (bp2.x == 0 && bp2.y == 0) { - bp2 = p2; + /** + * make a text input field for string options. + * @param defaultValue + * @param value + * @param path + * @private + */ + value: function _makeTextInput(defaultValue, value, path) { + var checkbox = document.createElement('input'); + checkbox.type = 'text'; + checkbox.className = 'vis-network-configuration text'; + checkbox.value = value; + if (value !== defaultValue) { + this.changedOptions.push({ path: path, value: value }); } - d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; - } - return d; - } - }; + var me = this; + checkbox.onchange = function () { + me._update(this.value, path); + }; - /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} - * @private - */ - Line._linear = function (data) { - // linear - var d = ''; - for (var i = 0; i < data.length; i++) { - if (i == 0) { - d += data[i].x + ',' + data[i].y; - } else { - d += ' ' + data[i].x + ',' + data[i].y; + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, checkbox); } - } - return d; - }; + }, { + key: '_makeColorField', - module.exports = Line; + /** + * make a color field with a color picker for color fields + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeColorField(arr, value, path) { + var _this2 = this; -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { + var defaultColor = arr[1]; + var div = document.createElement('div'); + value = value === undefined ? defaultColor : value; - 'use strict'; + if (value !== 'none') { + div.className = 'vis-network-configuration colorBlock'; + div.style.backgroundColor = value; + } else { + div.className = 'vis-network-configuration colorBlock none'; + } - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(49); + value = value === undefined ? defaultColor : value; + div.onclick = function () { + _this2._showColorPicker(value, div, path); + }; - function Bargraph(groupId, options) { - this.groupId = groupId; - this.options = options; - } + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, div); + } + }, { + key: '_showColorPicker', - Bargraph.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + /** + * used by the color buttons to call the color picker. + * @param event + * @param value + * @param div + * @param path + * @private + */ + value: function _showColorPicker(value, div, path) { + var _this3 = this; - Bargraph.prototype.getData = function (groupData) { - var combinedData = []; - for (var j = 0; j < groupData.length; j++) { - combinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: this.groupId - }); - } - return combinedData; - }; - - /** - * draw a bar graph - * - * @param groupIds - * @param processedGroupData - */ - Bargraph.draw = function (groupIds, processedGroupData, framework) { - var combinedData = []; - var intersections = {}; - var coreDistance; - var key, drawData; - var group; - var i, j; - var barPoints = 0; - - // combine all barchart data - for (i = 0; i < groupIds.length; i++) { - group = framework.groups[groupIds[i]]; - if (group.options.style === 'bar') { - if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] === true)) { - for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { - combinedData.push({ - x: processedGroupData[groupIds[i]][j].x, - y: processedGroupData[groupIds[i]][j].y, - groupId: groupIds[i], - label: processedGroupData[groupIds[i]][j].label - }); - barPoints += 1; - } - } + var rect = div.getBoundingClientRect(); + var bodyRect = document.body.getBoundingClientRect(); + var pickerX = rect.left + rect.width + 5; + var pickerY = rect.top - bodyRect.top + rect.height * 0.5; + this.colorPicker.show(pickerX, pickerY); + this.colorPicker.setColor(value); + this.colorPicker.setCallback(function (color) { + var colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')'; + div.style.backgroundColor = colorString; + _this3._update(colorString, path); + }); } - } - - if (barPoints === 0) { - return; - } + }, { + key: '_handleObject', - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; - } - }); + /** + * parse an object and draw the correct items + * @param obj + * @param path + * @private + */ + value: function _handleObject(obj) { + var path = arguments[1] === undefined ? [] : arguments[1]; - // get intersections - Bargraph._getDataIntersections(intersections, combinedData); + for (var subObj in obj) { + if (obj.hasOwnProperty(subObj)) { + var item = obj[subObj]; + var newPath = util.copyAndExtendArray(path, subObj); + var value = this._getValue(newPath); - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = framework.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; + if (item instanceof Array) { + this._handleArray(item, value, newPath); + } else if (typeof item === 'string') { + this._makeTextInput(item, value, newPath); + } else if (typeof item === 'boolean') { + this._makeCheckbox(item, value, newPath); + } else if (item instanceof Object) { + // collapse the physics options that are not enabled + var draw = true; + if (path.indexOf('physics') !== -1) { + if (this.moduleOptions.physics.solver !== subObj) { + draw = false; + } + } - key = combinedData[i].x; - var heightOffset = 0; - if (intersections[key] === undefined) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - key); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - } else { - var nextKey = i + (intersections[key].amount - intersections[key].resolved); - var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) { - coreDistance = Math.abs(combinedData[nextKey].x - key); - } - if (prevKey > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + if (draw === true) { + // initially collapse options with an disabled enabled option. + if (item.enabled !== undefined) { + var enabledPath = util.copyAndExtendArray(newPath, 'enabled'); + var enabledValue = this._getValue(enabledPath); + if (enabledValue === true) { + var label = this._makeLabel(subObj, newPath, true); + this._makeItem(newPath, label); + this._handleObject(item, newPath); + } else { + this._makeCheckbox(item, enabledValue, newPath); + } + } else { + var label = this._makeLabel(subObj, newPath, true); + this._makeItem(newPath, label); + this._handleObject(item, newPath); + } + } + } else { + console.error('dont know how to handle', item, subObj, newPath); + } + } } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; + } + }, { + key: '_handleArray', - if (group.options.stack === true) { - if (combinedData[i].y < group.zeroPosition) { - heightOffset = intersections[key].accumulatedNegative; - intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y; - } else { - heightOffset = intersections[key].accumulatedPositive; - intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y; + /** + * handle the array type of option + * @param optionName + * @param arr + * @param value + * @param path + * @private + */ + value: function _handleArray(arr, value, path) { + if (typeof arr[0] === 'string' && arr[0] === 'color') { + this._makeColorField(arr, value, path); + if (arr[1] !== value) { + this.changedOptions.push({ path: path, value: value }); } - } else if (group.options.barChart.sideBySide === true) { - drawData.width = drawData.width / intersections[key].amount; - drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); - if (group.options.barChart.align === 'left') { - drawData.offset -= 0.5 * drawData.width; - } else if (group.options.barChart.align === 'right') { - drawData.offset += 0.5 * drawData.width; + } else if (typeof arr[0] === 'string') { + this._makeDropdown(arr, value, path); + if (arr[0] !== value) { + this.changedOptions.push({ path: path, value: value }); + } + } else if (typeof arr[0] === 'number') { + this._makeRange(arr, value, path); + if (arr[0] !== value) { + this.changedOptions.push({ path: path, value: Number(value) }); } } } - DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg); - // draw points - if (group.options.drawPoints.enabled === true) { - Points.draw([combinedData[i]], group, framework, drawData.offset); - //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); - } - } - }; + }, { + key: '_update', - /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private - */ - Bargraph._getDataIntersections = function (intersections, combinedData) { - // get intersections - var coreDistance; - for (var i = 0; i < combinedData.length; i++) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); + /** + * called to update the network with the new settings. + * @param value + * @param path + * @private + */ + value: function _update(value, path) { + var options = this._constructOptions(value, path); + this.parent.setOptions(options); } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); + }, { + key: '_constructOptions', + value: function _constructOptions(value, path) { + var optionsObj = arguments[2] === undefined ? {} : arguments[2]; + + var pointer = optionsObj; + + // when dropdown boxes can be string or boolean, we typecast it into correct types + value = value === 'true' ? true : value; + value = value === 'false' ? false : value; + + for (var i = 0; i < path.length; i++) { + if (path[i] !== 'global') { + if (pointer[path[i]] === undefined) { + pointer[path[i]] = {}; + } + if (i !== path.length - 1) { + pointer = pointer[path[i]]; + } else { + pointer[path[i]] = value; + } + } + } + return optionsObj; } - if (coreDistance === 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; + }, { + key: '_printOptions', + value: function _printOptions() { + var options = {}; + for (var i = 0; i < this.changedOptions.length; i++) { + this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options); } - intersections[combinedData[i].x].amount += 1; + this.optionsContainer.innerHTML = '
var options = ' + JSON.stringify(options, null, 2) + '
'; } - } - }; + }]); - /** - * Get the width and offset for bargraphs based on the coredistance between datapoints - * - * @param coreDistance - * @param group - * @param minWidth - * @returns {{width: Number, offset: Number}} - * @private - */ - Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; - - offset = 0; // recalculate offset with the new width; - if (group.options.barChart.align === 'left') { - offset -= 0.5 * coreDistance; - } else if (group.options.barChart.align === 'right') { - offset += 0.5 * coreDistance; - } - } else { - // default settings - width = group.options.barChart.width; - offset = 0; - if (group.options.barChart.align === 'left') { - offset -= 0.5 * group.options.barChart.width; - } else if (group.options.barChart.align === 'right') { - offset += 0.5 * group.options.barChart.width; - } - } - - return { width: width, offset: offset }; - }; - - Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { - if (combinedData.length > 0) { - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; - } - }); - var intersections = {}; - - Bargraph._getDataIntersections(intersections, combinedData); - groupRanges[groupLabel] = Bargraph._getStackedYRange(intersections, combinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; - - Bargraph._getStackedYRange = function (intersections, combinedData) { - var key; - var yMin = combinedData[0].y; - var yMax = combinedData[0].y; - for (var i = 0; i < combinedData.length; i++) { - key = combinedData[i].x; - if (intersections[key] === undefined) { - yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; - yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; - } else { - if (combinedData[i].y < 0) { - intersections[key].accumulatedNegative += combinedData[i].y; - } else { - intersections[key].accumulatedPositive += combinedData[i].y; - } - } - } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; - yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; - yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; - yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; - } - } - - return { min: yMin, max: yMax }; - }; + return ConfigurationSystem; + })(); - module.exports = Bargraph; + exports['default'] = ConfigurationSystem; + module.exports = exports['default']; /***/ }, -/* 49 */ +/* 46 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var DOMutil = __webpack_require__(2); - - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; - } - - Points.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; - - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); - }; - - /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] - */ - Points.draw = function (dataset, group, framework, offset) { - if (offset === undefined) { - offset = 0; - } - for (var i = 0; i < dataset.length; i++) { - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); - } - }; - - module.exports = Points; - -/***/ }, -/* 50 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - Object.defineProperty(exports, "__esModule", { + Object.defineProperty(exports, '__esModule', { value: true }); - 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; }; })(); + 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; }; })(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var util = __webpack_require__(1); + var errorFound = false; + var allOptions = undefined; + var printStyle = 'background: #FFeeee; color: #dd0000'; /** - * @class Groups - * This class can store groups and options specific for groups. + * Used to validate options. */ - var Groups = (function () { - function Groups() { - _classCallCheck(this, Groups); - - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - - this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint - - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint - - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } } // 20:bright red - ]; - - this.options = {}; - this.defaultOptions = { - useDefaultGroups: true - }; - util.extend(this.options, this.defaultOptions); + var Validator = (function () { + function Validator() { + _classCallCheck(this, Validator); } - _createClass(Groups, [{ - key: "setOptions", - value: function setOptions(options) { - var optionFields = ["useDefaultGroups"]; - - if (options !== undefined) { - for (var groupName in options) { - if (options.hasOwnProperty(groupName)) { - if (optionFields.indexOf(groupName) === -1) { - var group = options[groupName]; - this.add(groupName, group); - } - } - } - } - } - }, { - key: "clear", + _createClass(Validator, null, [{ + key: 'validate', /** - * Clear all groups + * Main function to be called + * @param options + * @param subObject + * @returns {boolean} */ - value: function clear() { - this.groups = {}; - this.groupsArray = []; + value: function validate(options, referenceOptions, subObject) { + errorFound = false; + allOptions = referenceOptions; + var usedOptions = referenceOptions; + if (subObject !== undefined) { + usedOptions = referenceOptions[subObject]; + } + Validator.parse(options, usedOptions, []); + return errorFound; } }, { - key: "get", + key: 'parse', /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options + * Will traverse an object recursively and check every value + * @param options + * @param referenceOptions + * @param path */ - value: function get(groupname) { - var group = this.groups[groupname]; - if (group === undefined) { - if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } else { - // create new group - var index = this.defaultIndex % this.defaultGroups.length; - this.defaultIndex++; - group = {}; - group.color = this.defaultGroups[index]; - this.groups[groupname] = group; + value: function parse(options, referenceOptions, path) { + for (var option in options) { + if (options.hasOwnProperty(option)) { + Validator.check(option, options, referenceOptions, path); } } - - return group; } }, { - key: "add", + key: 'check', /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object + * Check every value. If the value is an object, call the parse function on that object. + * @param option + * @param options + * @param referenceOptions + * @param path */ - value: function add(groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; + value: function check(option, options, referenceOptions, path) { + if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) { + Validator.getSuggestion(option, referenceOptions, path); + } else if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) { + // __any__ is a wildcard. Any value is accepted and will be further analysed by reference. + if (Validator.getType(options[option]) === 'object') { + Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'].__type__, path); + } + } else { + // Since all options in the reference are objects, we can check whether they are supposed to be object to look for the __type__ field. + if (referenceOptions[option].__type__ !== undefined) { + // if this should be an object, we check if the correct type has been supplied to account for shorthand options. + Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option].__type__, path); + } else { + Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option], path); + } + } } - }]); + }, { + key: 'checkFields', - return Groups; - })(); + /** + * + * @param {String} option | the option property + * @param {Object} options | The supplied options object + * @param {Object} referenceOptions | The reference options containing all options and their allowed formats + * @param {String} referenceOption | Usually this is the same as option, except when handling an __any__ tag. + * @param {String} refOptionType | This is the type object from the reference options + * @param {Array} path | where in the object is the option + */ + value: function checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) { + var optionType = Validator.getType(options[option]); + var refOptionType = refOptionObj[optionType]; + if (refOptionType !== undefined) { + // if the type is correct, we check if it is supposed to be one of a few select values + if (Validator.getType(refOptionType) === 'array') { + if (refOptionType.indexOf(options[option]) === -1) { + console.log('%cInvalid option detected in "' + option + '".' + ' Allowed values are:' + Validator.print(refOptionType) + ' not "' + options[option] + '". ' + Validator.printLocation(path, option), printStyle); + errorFound = true; + } else if (optionType === 'object') { + path = util.copyAndExtendArray(path, option); + Validator.parse(options[option], referenceOptions[referenceOption], path); + } + } else if (optionType === 'object') { + path = util.copyAndExtendArray(path, option); + Validator.parse(options[option], referenceOptions[referenceOption], path); + } + } else { + if (refOptionObj['undef'] !== undefined && optionType === 'undefined') {} else if (refOptionObj['fn'] !== undefined && optionType === 'function') {} else { + // type of the field is incorrect + console.log('%cInvalid type received for "' + option + '". Expected: ' + Validator.print(Object.keys(refOptionObj)) + '. Received [' + optionType + '] "' + options[option] + '"' + Validator.printLocation(path, option), printStyle); + errorFound = true; + } + } + } + }, { + key: 'getType', + value: function getType(object) { + var type = typeof object; - exports["default"] = Groups; - module.exports = exports["default"]; + if (type === 'object') { + if (object === null) { + return 'null'; + } + if (object instanceof Boolean) { + return 'boolean'; + } + if (object instanceof Number) { + return 'number'; + } + if (object instanceof String) { + return 'string'; + } + if (Array.isArray(object)) { + return 'array'; + } + if (object instanceof Date) { + return 'date'; + } + if (object.nodeType !== undefined) { + return 'dom'; + } + if (object._isAMomentObject === true) { + return 'moment'; + } + return 'object'; + } else if (type === 'number') { + return 'number'; + } else if (type === 'boolean') { + return 'boolean'; + } else if (type === 'string') { + return 'string'; + } else if (type === undefined) { + return 'undefined'; + } + return type; + } + }, { + key: 'getSuggestion', + value: function getSuggestion(option, options, path) { + var localSearch = Validator.findInOptions(option, options, path, false); + var globalSearch = Validator.findInOptions(option, allOptions, [], true); -/***/ }, -/* 51 */ -/***/ function(module, exports, __webpack_require__) { + var localSearchThreshold = 8; + var globalSearchThreshold = 5; - 'use strict'; + if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) { + console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was misplaced? Matching option found at: ' + Validator.printLocation(globalSearch.path, option, ''), printStyle); + } else if (localSearch.distance <= localSearchThreshold) { + console.log('%cUnknown option detected: "' + option + '". Did you mean "' + localSearch.closestMatch + '"?' + Validator.printLocation(localSearch.path, option), printStyle); + } else { + console.log('%cUnknown option detected: "' + option + '". Did you mean one of these: ' + Validator.print(Object.keys(options)) + Validator.printLocation(path, option), printStyle); + } - Object.defineProperty(exports, '__esModule', { - value: true - }); + errorFound = true; + } + }, { + key: 'findInOptions', - 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; }; })(); + /** + * traverse the options in search for a match. + * @param option + * @param options + * @param path + * @param recursive + * @returns {{closestMatch: string, path: Array, distance: number}} + */ + value: function findInOptions(option, options, path) { + var recursive = arguments[3] === undefined ? false : arguments[3]; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + var min = 1000000000; + var closestMatch = ''; + var closestMatchPath = []; + for (var op in options) { + var type = Validator.getType(options[op]); + var distance = undefined; + if (type === 'object' && recursive === true) { + var result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path, op)); + if (min > result.distance) { + closestMatch = result.closestMatch; + closestMatchPath = result.path; + min = result.distance; + } + } else { + distance = Validator.levenshteinDistance(option, op); + if (min > distance) { + closestMatch = op; + closestMatchPath = util.copyArray(path); + min = distance; + } + } + } + return { closestMatch: closestMatch, path: closestMatchPath, distance: min }; + } + }, { + key: 'printLocation', + value: function printLocation(path, option) { + var prefix = arguments[2] === undefined ? 'Problem value found at: \n' : arguments[2]; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + var str = '\n\n' + prefix + 'options = {\n'; + for (var i = 0; i < path.length; i++) { + for (var j = 0; j < i + 1; j++) { + str += ' '; + } + str += path[i] + ': {\n'; + } + for (var j = 0; j < path.length + 1; j++) { + str += ' '; + } + str += option + '\n'; + for (var i = 0; i < path.length + 1; i++) { + for (var j = 0; j < path.length - i; j++) { + str += ' '; + } + str += '}\n'; + } + return str + '\n\n'; + } + }, { + key: 'print', + value: function print(options) { + return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, '').replace(/(\,)/g, ', '); + } + }, { + key: 'levenshteinDistance', - var _componentsNode = __webpack_require__(71); + // Compute the edit distance between the two given strings + // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript + /* + Copyright (c) 2011 Andrei Mackenzie + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + value: function levenshteinDistance(a, b) { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; - var _componentsNode2 = _interopRequireDefault(_componentsNode); + var matrix = []; - var _componentsSharedLabel = __webpack_require__(72); + // increment along the first column of each row + var i; + for (i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + // increment each column in the first row + var j; + for (j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); + // Fill in the rest of the matrix + for (i = 1; i <= b.length; i++) { + for (j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) == a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution + Math.min(matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1)); // deletion + } + } + } - var NodesHandler = (function () { - function NodesHandler(body, images, groups, layoutEngine) { - var _this = this; + return matrix[b.length][a.length]; + } + }]); - _classCallCheck(this, NodesHandler); + return Validator; + })(); - this.body = body; - this.images = images; - this.groups = groups; - this.layoutEngine = layoutEngine; - - // create the node API in the body container - this.body.functions.createNode = this.create.bind(this); + exports['default'] = Validator; + exports.printStyle = printStyle; - this.nodesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items, params.data); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; + // item is undefined, which is allowed - this.options = {}; - this.defaultOptions = { - borderWidth: 1, - borderWidthSelected: undefined, - brokenImage: undefined, - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - }, - hover: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 0, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - group: undefined, - hidden: false, - icon: { - face: 'FontAwesome', //'FontAwesome', - code: undefined, //'\uf007', - size: 50, //50, - color: '#2B7CE9' //'#aa00ff' - }, - image: undefined, // --> URL - label: undefined, - level: undefined, - mass: 1, - physics: true, - scaling: { - min: 10, - max: 30, - label: { - enabled: false, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 - }, - customScalingFunction: function customScalingFunction(min, max, total, value) { - if (max === min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - shape: 'ellipse', - size: 25, - title: undefined, - value: undefined, - x: undefined, - y: undefined - }; - util.extend(this.options, this.defaultOptions); + // item is a function, which is allowed - this.bindEventListeners(); - } +/***/ }, +/* 47 */ +/***/ function(module, exports, __webpack_require__) { - _createClass(NodesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; + 'use strict'; - // refresh the nodes. Used when reverting from hierarchical layout - this.body.emitter.on('refreshNodes', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createNode; - delete _this2.nodesListeners.add; - delete _this2.nodesListeners.update; - delete _this2.nodesListeners.remove; - delete _this2.nodesListeners; - }); - } - }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - _componentsNode2['default'].parseOptions(this.options, options); + Object.defineProperty(exports, '__esModule', { + value: true + }); + /** + * This object contains all possible options. It will check if the types are correct, if required if the option is one + * of the allowed values. + * + * __any__ means that the name of the property does not matter. + * __type__ is a required field for all objects and contains the allowed types of all objects + */ + var string = 'string'; + var boolean = 'boolean'; + var number = 'number'; + var array = 'array'; + var date = 'date'; + var object = 'object'; // should only be in a __type__ property + var dom = 'dom'; + var moment = 'moment'; + var fn = 'function'; + var nada = 'null'; + var undef = 'undefined'; + var any = 'any'; - // update the shape in all nodes - if (options.shape !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateShape(); - } - } - } + var allOptions = { + configure: { + enabled: { boolean: boolean }, + filter: { boolean: boolean, string: string, array: array }, + container: { dom: dom }, + __type__: { object: object, boolean: boolean, string: string, array: array } + }, - // update the shape size in all nodes - if (options.font !== undefined) { - _componentsSharedLabel2['default'].parseOptions(this.options.font, options); - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateLabelModule(); - this.body.nodes[nodeId]._reset(); - } - } - } + //globals : + align: { string: string }, + autoResize: { boolean: boolean }, + clickToUse: { boolean: boolean }, + dataAttributes: { string: string, array: array }, + editable: { boolean: boolean, object: object }, + end: { number: number, date: date, string: string, moment: moment }, + format: { + minorLabels: { + millisecond: { string: string, undef: undef }, + second: { string: string, undef: undef }, + minute: { string: string, undef: undef }, + hour: { string: string, undef: undef }, + weekday: { string: string, undef: undef }, + day: { string: string, undef: undef }, + month: { string: string, undef: undef }, + year: { string: string, undef: undef }, + __type__: { object: object } + }, + majorLabels: { + millisecond: { string: string, undef: undef }, + second: { string: string, undef: undef }, + minute: { string: string, undef: undef }, + hour: { string: string, undef: undef }, + weekday: { string: string, undef: undef }, + day: { string: string, undef: undef }, + month: { string: string, undef: undef }, + year: { string: string, undef: undef }, + __type__: { object: object } + }, + __type__: { object: object } + }, + groupOrder: { string: string, fn: fn }, + height: { string: string, number: number }, + hiddenDates: { object: object, array: array }, + locale: { string: string }, + locales: { + __any__: { object: object }, + __type__: { object: object } + }, + margin: { + axis: { number: number }, + item: { + horizontal: { number: number }, + vertical: { number: number }, + __type__: { object: object, number: number } + }, + __type__: { object: object, number: number } + }, + max: { date: date, number: number, string: string, moment: moment }, + maxHeight: { number: number, string: string }, + min: { date: date, number: number, string: string, moment: moment }, + minHeight: { number: number, string: string }, + moveable: { boolean: boolean }, + multiselect: { boolean: boolean }, + onAdd: { fn: fn }, + onUpdate: { fn: fn }, + onMove: { fn: fn }, + onMoving: { fn: fn }, + onRemove: { fn: fn }, + order: { fn: fn }, + orientation: { + axis: { string: string }, + item: { string: string }, + __type__: { string: string, object: object } + }, + selectable: { boolean: boolean }, + showCurrentTime: { boolean: boolean }, + showMajorLabels: { boolean: boolean }, + showMinorLabels: { boolean: boolean }, + stack: { boolean: boolean }, + snap: { fn: fn, nada: nada }, + start: { date: date, number: number, string: string, moment: moment }, + template: { fn: fn }, + timeAxis: { + scale: { string: string }, + step: { number: number }, + __type__: { object: object } + }, + type: { string: string }, + width: { string: string, number: number }, + zoomable: { boolean: boolean }, + zoomMax: { number: number }, + zoomMin: { number: number }, - // update the shape size in all nodes - if (options.size !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId]._reset(); - } - } - } + __type__: { object: object } + }; - // update the state of the letiables if needed - if (options.hidden !== undefined || options.physics !== undefined) { - this.body.emitter.emit('_dataChanged'); - } + var configureOptions = { + global: { + align: ['center', 'left', 'right'], + autoResize: true, + clickToUse: false, + // dataAttributes: ['all'], // FIXME: can be 'all' or string[] + editable: { + add: false, + remove: false, + updateGroup: false, + updateTime: false + }, + end: '', + format: { + minorLabels: { + millisecond: 'SSS', + second: 's', + minute: 'HH:mm', + hour: 'HH:mm', + weekday: 'ddd D', + day: 'D', + month: 'MMM', + year: 'YYYY' + }, + majorLabels: { + millisecond: 'HH:mm:ss', + second: 'D MMMM HH:mm', + minute: 'ddd D MMMM', + hour: 'ddd D MMMM', + weekday: 'MMMM YYYY', + day: 'MMMM YYYY', + month: 'YYYY', + year: '' } - } - }, { - key: 'setData', + }, - /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private - */ - value: function setData(nodes) { - var _this3 = this; + //groupOrder: {string, fn}, + height: '', + //hiddenDates: {object, array}, + locale: '', + margin: { + axis: [20, 0, 100, 1], + item: { + horizontal: [10, 0, 100, 1], + vertical: [10, 0, 100, 1] + } + }, + max: '', + maxHeight: '', + min: '', + minHeight: '', + moveable: false, + multiselect: false, + //onAdd: {fn}, + //onUpdate: {fn}, + //onMove: {fn}, + //onMoving: {fn}, + //onRename: {fn}, + //order: {fn}, + orientation: { + axis: ['both', 'bottom', 'top'], + item: ['bottom', 'top'] + }, + selectable: true, + showCurrentTime: false, + showMajorLabels: true, + showMinorLabels: true, + stack: true, + //snap: {fn, nada}, + start: '', + //template: {fn}, + //timeAxis: { + // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'], + // step: [1, 1, 10, 1] + //}, + type: ['box', 'point', 'range', 'background'], + width: '100%', + zoomable: true, + zoomMax: [315360000000000, 10, 315360000000000, 1], + zoomMin: [10, 10, 315360000000000, 1] + } + }; - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + exports.allOptions = allOptions; + exports.configureOptions = configureOptions; - var oldNodesData = this.body.data.nodes; +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.body.data.nodes = nodes; - } else if (Array.isArray(nodes)) { - this.body.data.nodes = new DataSet(); - this.body.data.nodes.add(nodes); - } else if (!nodes) { - this.body.data.nodes = new DataSet(); - } else { - throw new TypeError('Array or DataSet expected'); - } + 'use strict'; - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); - } + var Hammer = __webpack_require__(41); - // remove drawn nodes - this.body.nodes = {}; + /** + * Register a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.onTouch = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFirst && !isTouching) { + callback(event); - if (this.body.data.nodes) { - (function () { - // subscribe to new dataset - var me = _this3; - util.forEach(_this3.nodesListeners, function (callback, event) { - me.body.data.nodes.on(event, callback); - }); + isTouching = true; + setTimeout(function () { + isTouching = false; + }, 0); + } + }; - // draw all new nodes - var ids = _this3.body.data.nodes.getIds(); - _this3.add(ids, true); - })(); - } + hammer.on('hammer.input', callback.inputHandler); + }; - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } + // isTouching is true while a touch action is being emitted + // this is a hack to prevent `touch` from being fired twice + var isTouching = false; + + /** + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal && !isReleasing) { + callback(event); + + isReleasing = true; + setTimeout(function () { + isReleasing = false; + }, 0); } - }, { - key: 'add', + }; - /** - * Add nodes - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + return hammer.on('hammer.input', callback.inputHandler); + }; - var id = undefined; - var newNodes = []; - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - var _properties = this.body.data.nodes.get(id); - var node = this.create(_properties); - newNodes.push(node); - this.body.nodes[id] = node; // note: this may replace an existing node - } + // isReleasing is true while a release action is being emitted + // this is a hack to prevent `release` from being fired twice + var isReleasing = false; - this.layoutEngine.positionInitially(newNodes); + /** + * Unregister a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offTouch = function (hammer, callback) { + hammer.off('hammer.input', callback.inputHandler); + }; - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'update', + /** + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offRelease = exports.offTouch; - /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids, changedData) { - var nodes = this.body.nodes; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node !== undefined) { - // update node - node.setOptions(data, this.constants); - } else { - dataChanged = true; - // create node - node = this.create(properties); - nodes[id] = node; - } - } +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); + // English + 'use strict'; + + exports['en'] = { + current: 'current', + time: 'time' + }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; + + // Dutch + exports['nl'] = { + current: 'huidige', + time: 'tijd' + }; + exports['nl_NL'] = exports['nl']; + exports['nl_BE'] = exports['nl']; + +/***/ }, +/* 50 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var DOMutil = __webpack_require__(2); + var Points = __webpack_require__(52); + + function Line(groupId, options) { + this.groupId = groupId; + this.options = options; + } + + Line.prototype.getData = function (groupData) { + var combinedData = []; + for (var j = 0; j < groupData.length; j++) { + combinedData.push({ + x: groupData[j].x, + y: groupData[j].y, + groupId: this.groupId + }); + } + return combinedData; + }; + + Line.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; + + Line.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { + if (combinedData.length > 0) { + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; } else { - this.body.emitter.emit('_dataUpdated'); + return a.x - b.x; } - } - }, { - key: 'remove', + }); + var intersections = {}; - /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var nodes = this.body.nodes; + Line._getDataIntersections(intersections, combinedData); + groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - delete nodes[id]; + Line._getStackedYRange = function (intersections, combinedData) { + var key; + var yMin = combinedData[0].y; + var yMax = combinedData[0].y; + for (var i = 0; i < combinedData.length; i++) { + key = combinedData[i].x; + if (intersections[key] === undefined) { + yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; + yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; + } else { + if (combinedData[i].y < 0) { + intersections[key].accumulatedNegative += combinedData[i].y; + } else { + intersections[key].accumulatedPositive += combinedData[i].y; } - - this.body.emitter.emit('_dataChanged'); } - }, { - key: 'create', + } + for (var xpos in intersections) { + if (intersections.hasOwnProperty(xpos)) { + yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; + yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; + yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; + yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; + } + } - /** - * create a node - * @param properties - * @param constructorClass - */ - value: function create(properties) { - var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; + return { min: yMin, max: yMax }; + }; - return new constructorClass(properties, this.body, this.images, this.groups, this.options); + /** + * Fill the intersections object with counters of how many datapoints share the same x coordinates + * @param intersections + * @param combinedData + * @private + */ + Line._getDataIntersections = function (intersections, combinedData) { + // get intersections + var coreDistance; + for (var i = 0; i < combinedData.length; i++) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); } - }, { - key: 'refresh', - value: function refresh() { - var nodes = this.body.nodes; - for (var nodeId in nodes) { - var node = undefined; - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - } - var data = this.body.data.nodes._data[nodeId]; - if (node !== undefined && data !== undefined) { - node.setOptions({ fixed: false }); - node.setOptions(data); - } - } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); } - }, { - key: 'getPositions', - - /** - * Returns the positions of the nodes. - * @param ids --> optional, can be array of nodeIds, can be string - * @returns {{}} - */ - value: function getPositions(ids) { - var dataArray = {}; - if (ids !== undefined) { - if (Array.isArray(ids) === true) { - for (var i = 0; i < ids.length; i++) { - if (this.body.nodes[ids[i]] !== undefined) { - var node = this.body.nodes[ids[i]]; - dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - if (this.body.nodes[ids] !== undefined) { - var node = this.body.nodes[ids]; - dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } + if (coreDistance === 0) { + if (intersections[combinedData[i].x] === undefined) { + intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; } - return dataArray; + intersections[combinedData[i].x].amount += 1; } - }, { - key: 'storePositions', + } + }; - /** - * Load the XY positions of the nodes into the dataset. - */ - value: function storePositions() { - // todo: add support for clusters and hierarchical. - var dataArray = []; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { - dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); - } - } + /** + * draw a line graph + * + * @param dataset + * @param group + */ + Line.prototype.draw = function (dataset, group, framework) { + if (dataset != null) { + if (dataset.length > 0) { + var path, d; + var svgHeight = Number(framework.svg.style.height.replace('px', '')); + path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + path.setAttributeNS(null, 'class', group.className); + if (group.style !== undefined) { + path.setAttributeNS(null, 'style', group.style); } - this.body.data.nodes.update(dataArray); - } - }, { - key: 'getBoundingBox', - /** - * get the bounding box of a node. - * @param nodeId - * @returns {j|*} - */ - value: function getBoundingBox(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].shape.boundingBox; + // construct path from dataset + if (group.options.catmullRom.enabled == true) { + d = Line._catmullRom(dataset, group); + } else { + d = Line._linear(dataset); } - } - }, { - key: 'getConnectedNodes', - /** - * Get the Ids of nodes connected to this node. - * @param nodeId - * @returns {Array} - */ - value: function getConnectedNodes(nodeId) { - var nodeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - var nodeObj = {}; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId === nodeId) { - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } - } else if (edge.fromId === nodeId) { - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId); - nodeObj[edge.toId] = true; - } - } + // append with points for fill and finalize the path + if (group.options.shaded.enabled == true) { + var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + var dFill; + if (group.options.shaded.orientation == 'top') { + dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0; + } else { + dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight; + } + fillPath.setAttributeNS(null, 'class', group.className + ' vis-fill'); + if (group.options.shaded.style !== undefined) { + fillPath.setAttributeNS(null, 'style', group.options.shaded.style); } + fillPath.setAttributeNS(null, 'd', dFill); } - return nodeList; - } - }, { - key: 'getEdges', + // copy properties to path for drawing. + path.setAttributeNS(null, 'd', 'M' + d); - /** - * Get the ids of the edges connected to this node. - * @param nodeId - * @returns {*} - */ - value: function getEdges(nodeId) { - var edgeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - for (var i = 0; i < node.edges.length; i++) { - edgeList.push(node.edges[i].id); - } + // draw points + if (group.options.drawPoints.enabled == true) { + Points.draw(dataset, group, framework); } - return nodeList; } - }]); - - return NodesHandler; - })(); - - exports['default'] = NodesHandler; - module.exports = exports['default']; - -/***/ }, -/* 52 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + } + }; - var _componentsEdge = __webpack_require__(73); + /** + * This uses an uniform parametrization of the CatmullRom algorithm: + * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. + * @param data + * @returns {string} + * @private + */ + Line._catmullRomUniform = function (data) { + // catmull rom + var p0, p1, p2, p3, bp1, bp2; + var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; + var normalization = 1 / 6; + var length = data.length; + for (var i = 0; i < length - 1; i++) { - var _componentsEdge2 = _interopRequireDefault(_componentsEdge); + p0 = i == 0 ? data[0] : data[i - 1]; + p1 = data[i]; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - var _componentsSharedLabel = __webpack_require__(72); + // Catmull-Rom to Cubic Bezier conversion matrix + // 0 1 0 0 + // -1/6 1 1/6 0 + // 0 1/6 1 -1/6 + // 0 0 1 0 - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + // bp0 = { x: p1.x, y: p1.y }; + bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; + bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; + // bp0 = { x: p2.x, y: p2.y }; - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); + d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; + } - var EdgesHandler = (function () { - function EdgesHandler(body, images, groups) { - var _this = this; + return d; + }; - _classCallCheck(this, EdgesHandler); + /** + * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. + * By default, the centripetal parameterization is used because this gives the nicest results. + * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. + * + * One optimization can be used to reuse distances since this is a sliding window approach. + * @param data + * @param group + * @returns {string} + * @private + */ + Line._catmullRom = function (data, group) { + var alpha = group.options.catmullRom.alpha; + if (alpha == 0 || alpha === undefined) { + return this._catmullRomUniform(data); + } else { + var p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M; + var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; + var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' '; + var length = data.length; + for (var i = 0; i < length - 1; i++) { - this.body = body; - this.images = images; - this.groups = groups; + p0 = i == 0 ? data[0] : data[i - 1]; + p1 = data[i]; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - // create the edge API in the body container - this.body.functions.createEdge = this.create.bind(this); - - this.edgesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; - - this.options = {}; - this.defaultOptions = { - arrows: { - to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: 1 }, - from: { enabled: false, scaleFactor: 1 } - }, - color: { - color: '#848484', - highlight: '#848484', - hover: '#848484', - inherit: 'from', - opacity: 1 - }, - dashes: false, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 0, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - hidden: false, - hoverWidth: 1.5, - label: undefined, - length: undefined, - physics: true, - scaling: { - min: 1, - max: 15, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 - }, - customScalingFunction: function customScalingFunction(min, max, total, value) { - if (max === min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - selectionWidth: 1, - selfReferenceSize: 20, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - smooth: { - enabled: true, - dynamic: true, - type: 'continuous', - roundness: 0.5 - }, - title: undefined, - width: 1, - value: undefined - }; - - util.extend(this.options, this.defaultOptions); - - this.bindEventListeners(); - } - - _createClass(EdgesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; - - // this allows external modules to force all dynamic curves to turn static. - this.body.emitter.on('_forceDisableDynamicCurves', function (type) { - var emitChange = false; - for (var edgeId in _this2.body.edges) { - if (_this2.body.edges.hasOwnProperty(edgeId)) { - var edge = _this2.body.edges[edgeId]; - var edgeData = _this2.body.data.edges._data[edgeId]; - - // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. - // this is because a change in the global would not affect these curves. - if (edgeData !== undefined) { - var edgeOptions = edgeData.smooth; - if (edgeOptions !== undefined) { - if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { - if (type === undefined) { - edge.setOptions({ smooth: false }); - } else { - edge.setOptions({ smooth: { dynamic: false, type: type } }); - } - emitChange = true; - } - } - } - } - } - if (emitChange === true) { - _this2.body.emitter.emit('_dataChanged'); - } - }); - - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on('_dataUpdated', function () { - _this2.reconnectEdges(); - _this2.markAllEdgesAsDirty(); - }); - - // refresh the edges. Used when reverting from hierarchical layout - this.body.emitter.on('refreshEdges', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createEdge; - delete _this2.edgesListeners.add; - delete _this2.edgesListeners.update; - delete _this2.edgesListeners.remove; - delete _this2.edgesListeners; - }); - } - }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - // use the parser from the Edge class to fill in all shorthand notations - _componentsEdge2['default'].parseOptions(this.options, options); - - // hanlde multiple input cases for color - if (options.color !== undefined) { - this.markAllEdgesAsDirty(); - } - - // update smooth settings in all edges - var dataChanged = false; - if (options.smooth !== undefined) { - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; - } - } - } - - // update fonts in all edges - if (options.font !== undefined) { - // use the parser from the Label class to fill in all shorthand notations - _componentsSharedLabel2['default'].parseOptions(this.options, options); - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - this.body.edges[edgeId].updateLabelModule(); - } - } - } - - // update the state of the variables if needed - if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } - } - } - }, { - key: 'setData', - - /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ - value: function setData(edges) { - var _this3 = this; - - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var oldEdgesData = this.body.data.edges; - - if (edges instanceof DataSet || edges instanceof DataView) { - this.body.data.edges = edges; - } else if (Array.isArray(edges)) { - this.body.data.edges = new DataSet(); - this.body.data.edges.add(edges); - } else if (!edges) { - this.body.data.edges = new DataSet(); - } else { - throw new TypeError('Array or DataSet expected'); - } - - // TODO: is this null or undefined or false? - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); - } - - // remove drawn edges - this.body.edges = {}; - - // TODO: is this null or undefined or false? - if (this.body.data.edges) { - // subscribe to new dataset - util.forEach(this.edgesListeners, function (callback, event) { - _this3.body.data.edges.on(event, callback); - }); - - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this.add(ids, true); - } - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'add', - - /** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var edges = this.body.edges; - var edgesData = this.body.data.edges; - - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } - - var data = edgesData.get(id, { 'showInternalIds': true }); - edges[id] = this.create(data); - } - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'update', - - /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var data = edgesData.get(id); - var edge = edges[id]; - if (edge === null) { - // update edge - edge.disconnect(); - dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. - edge.connect(); - } else { - // create edge - this.body.edges[id] = this.create(data); - dataChanged = true; - } - } - - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } else { - this.body.emitter.emit('_dataUpdated'); - } - } - }, { - key: 'remove', - - /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var edges = this.body.edges; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge !== undefined) { - if (edge.via != null) { - delete this.body.supportNodes[edge.via.id]; - } - edge.disconnect(); - delete edges[id]; - } - } - - this.body.emitter.emit('_dataChanged'); - } - }, { - key: 'refresh', - value: function refresh() { - var edges = this.body.edges; - for (var edgeId in edges) { - var edge = undefined; - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - } - var data = this.body.data.edges._data[edgeId]; - if (edge !== undefined && data !== undefined) { - edge.setOptions(data); - } - } - } - }, { - key: 'create', - value: function create(properties) { - return new _componentsEdge2['default'](properties, this.body, this.options); - } - }, { - key: 'markAllEdgesAsDirty', - value: function markAllEdgesAsDirty() { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].edgeType.colorDirty = true; - } - } - }, { - key: 'reconnectEdges', - - /** - * Reconnect all edges - * @private - */ - value: function reconnectEdges() { - var id; - var nodes = this.body.nodes; - var edges = this.body.edges; - - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } - - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } - } - }]); - - return EdgesHandler; - })(); - - exports['default'] = EdgesHandler; - module.exports = exports['default']; - -/***/ }, -/* 53 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var _componentsPhysicsBarnesHutSolver = __webpack_require__(74); - - var _componentsPhysicsBarnesHutSolver2 = _interopRequireDefault(_componentsPhysicsBarnesHutSolver); - - var _componentsPhysicsRepulsionSolver = __webpack_require__(75); - - var _componentsPhysicsRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsRepulsionSolver); - - var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(76); - - var _componentsPhysicsHierarchicalRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalRepulsionSolver); - - var _componentsPhysicsSpringSolver = __webpack_require__(77); - - var _componentsPhysicsSpringSolver2 = _interopRequireDefault(_componentsPhysicsSpringSolver); - - var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(78); - - var _componentsPhysicsHierarchicalSpringSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalSpringSolver); - - var _componentsPhysicsCentralGravitySolver = __webpack_require__(79); - - var _componentsPhysicsCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsCentralGravitySolver); - - var util = __webpack_require__(1); - - var PhysicsEngine = (function () { - function PhysicsEngine(body) { - _classCallCheck(this, PhysicsEngine); - - this.body = body; - this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; - - this.physicsEnabled = true; - this.simulationInterval = 1000 / 60; - this.requiresTimeout = true; - this.previousStates = {}; - this.freezeCache = {}; - this.renderTimer = undefined; - - this.stabilized = false; - this.stabilizationIterations = 0; - this.ready = false; // will be set to true if the stabilize - - // default options - this.options = {}; - this.defaultOptions = { - barnesHut: { - theta: 0.5, - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0.2, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - centralGravity: 0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 120, - damping: 0.09 - }, - maxVelocity: 50, - minVelocity: 0.1, // px/s - solver: 'barnesHut', - stabilization: { - enabled: true, - iterations: 1000, // maximum number of iteration to stabilize - updateInterval: 100, - onlyDynamicEdges: false, - fit: true - }, - timestep: 0.5 - }; - util.extend(this.options, this.defaultOptions); - - this.bindEventListeners(); - } - - _createClass(PhysicsEngine, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this = this; - - this.body.emitter.on('initPhysics', function () { - _this.initPhysics(); - }); - this.body.emitter.on('resetPhysics', function () { - _this.stopSimulation();_this.ready = false; - }); - this.body.emitter.on('disablePhysics', function () { - _this.physicsEnabled = false;_this.stopSimulation(); - }); - this.body.emitter.on('restorePhysics', function () { - _this.setOptions(_this.options); - if (_this.ready === true) { - _this.startSimulation(); - } - }); - this.body.emitter.on('startSimulation', function () { - if (_this.ready === true) { - _this.startSimulation(); - } - }); - this.body.emitter.on('stopSimulation', function () { - _this.stopSimulation(); - }); - this.body.emitter.on('destroy', function () { - _this.stopSimulation(false); - _this.body.emitter.off(); - }); - } - }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - if (options === false) { - this.physicsEnabled = false; - this.stopSimulation(); - } else { - this.physicsEnabled = true; - util.selectiveNotDeepExtend(['stabilization'], this.options, options); - util.mergeOptions(this.options, options, 'stabilization'); - } - } - - this.init(); - } - }, { - key: 'init', - value: function init() { - var options; - if (this.options.solver === 'repulsion') { - options = this.options.repulsion; - this.nodesSolver = new _componentsPhysicsRepulsionSolver2['default'](this.body, this.physicsBody, options); - this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); - } else if (this.options.solver === 'hierarchicalRepulsion') { - options = this.options.hierarchicalRepulsion; - this.nodesSolver = new _componentsPhysicsHierarchicalRepulsionSolver2['default'](this.body, this.physicsBody, options); - this.edgesSolver = new _componentsPhysicsHierarchicalSpringSolver2['default'](this.body, this.physicsBody, options); - } else { - // barnesHut - options = this.options.barnesHut; - this.nodesSolver = new _componentsPhysicsBarnesHutSolver2['default'](this.body, this.physicsBody, options); - this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); - } - - this.gravitySolver = new _componentsPhysicsCentralGravitySolver2['default'](this.body, this.physicsBody, options); - this.modelOptions = options; - } - }, { - key: 'initPhysics', - value: function initPhysics() { - if (this.physicsEnabled === true) { - if (this.options.stabilization.enabled === true) { - this.stabilize(); - } else { - this.stabilized = false; - this.ready = true; - this.body.emitter.emit('fit', {}, true); - this.startSimulation(); - } - } else { - this.ready = true; - this.body.emitter.emit('fit'); - } - } - }, { - key: 'startSimulation', + d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); + d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); + d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); - /** - * Start the simulation - */ - value: function startSimulation() { - if (this.physicsEnabled === true) { - this.stabilized = false; - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on('initRedraw', this.viewFunction); - this.body.emitter.emit('_startRendering'); - } - } else { - this.body.emitter.emit('_redraw'); - } - } - }, { - key: 'stopSimulation', + // Catmull-Rom to Cubic Bezier conversion matrix - /** - * Stop the simulation, force stabilization. - */ - value: function stopSimulation() { - var emit = arguments[0] === undefined ? true : arguments[0]; + // A = 2d1^2a + 3d1^a * d2^a + d3^2a + // B = 2d3^2a + 3d3^a * d2^a + d2^2a - this.stabilized = true; - if (emit === true) { - this._emitStabilized(); + // [ 0 1 0 0 ] + // [ -d2^2a /N A/N d1^2a /N 0 ] + // [ 0 d3^2a /M B/M -d2^2a /M ] + // [ 0 0 1 0 ] + + d3powA = Math.pow(d3, alpha); + d3pow2A = Math.pow(d3, 2 * alpha); + d2powA = Math.pow(d2, alpha); + d2pow2A = Math.pow(d2, 2 * alpha); + d1powA = Math.pow(d1, alpha); + d1pow2A = Math.pow(d1, 2 * alpha); + + A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; + B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; + N = 3 * d1powA * (d1powA + d2powA); + if (N > 0) { + N = 1 / N; } - if (this.viewFunction !== undefined) { - this.body.emitter.off('initRedraw', this.viewFunction); - this.viewFunction = undefined; - if (emit === true) { - this.body.emitter.emit('_stopRendering'); - } + M = 3 * d3powA * (d3powA + d2powA); + if (M > 0) { + M = 1 / M; } - } - }, { - key: 'simulationStep', - - /** - * The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized. - * - */ - value: function simulationStep() { - // check if the physics have settled - var startTime = Date.now(); - this.physicsTick(); - var physicsTime = Date.now() - startTime; - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed === true) && this.stabilized === false) { - this.physicsTick(); + bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, + y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; - } + bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, + y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; - if (this.stabilized === true) { - if (this.stabilizationIterations > 1) { - // trigger the 'stabilized' event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - this.stabilizationIterations = 0; - this.startedStabilization = false; - this._emitStabilized(); - } else { - this.stabilizationIterations = 0; - } - this.stopSimulation(); + if (bp1.x == 0 && bp1.y == 0) { + bp1 = p1; } - } - }, { - key: '_emitStabilized', - value: function _emitStabilized() { - var _this2 = this; - - if (this.stabilizationIterations > 1) { - setTimeout(function () { - _this2.body.emitter.emit('stabilized', { iterations: _this2.stabilizationIterations }); - }, 0); + if (bp2.x == 0 && bp2.y == 0) { + bp2 = p2; } + d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; } - }, { - key: 'physicsTick', - - /** - * A single simulation step (or 'tick') in the physics simulation - * - * @private - */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); - // determine if the network has stabilzied - if (this.stabilized === true) { - this.revert(); - } else { - // this is here to ensure that there is no start event when the network is already stable. - if (this.startedStabilization === false) { - this.body.emitter.emit('startStabilizing'); - this.startedStabilization = true; - } - } + return d; + } + }; - this.stabilizationIterations++; - } + /** + * this generates the SVG path for a linear drawing between datapoints. + * @param data + * @returns {string} + * @private + */ + Line._linear = function (data) { + // linear + var d = ''; + for (var i = 0; i < data.length; i++) { + if (i == 0) { + d += data[i].x + ',' + data[i].y; + } else { + d += ' ' + data[i].x + ',' + data[i].y; } - }, { - key: 'updatePhysicsIndices', + } + return d; + }; - /** - * Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time. - * - * @private - */ - value: function updatePhysicsIndices() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - var nodes = this.body.nodes; - var edges = this.body.edges; + module.exports = Line; - // get node indices for physics - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.physics === true) { - this.physicsBody.physicsNodeIndices.push(nodeId); - } - } - } +/***/ }, +/* 51 */ +/***/ function(module, exports, __webpack_require__) { - // get edge indices for physics - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.physics === true) { - this.physicsBody.physicsEdgeIndices.push(edgeId); - } - } - } + 'use strict'; - // get the velocity and the forces vector - for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - var nodeId = this.physicsBody.physicsNodeIndices[i]; - this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; + var DOMutil = __webpack_require__(2); + var Points = __webpack_require__(52); - // forces can be reset because they are recalculated. Velocities have to persist. - if (this.physicsBody.velocities[nodeId] === undefined) { - this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; - } - } + function Bargraph(groupId, options) { + this.groupId = groupId; + this.options = options; + } - // clean deleted nodes from the velocity vector - for (var nodeId in this.physicsBody.velocities) { - if (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } - } - } - }, { - key: 'revert', + Bargraph.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; - /** - * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized. - */ - value: function revert() { - var nodeIds = Object.keys(this.previousStates); - var nodes = this.body.nodes; - var velocities = this.physicsBody.velocities; + Bargraph.prototype.getData = function (groupData) { + var combinedData = []; + for (var j = 0; j < groupData.length; j++) { + combinedData.push({ + x: groupData[j].x, + y: groupData[j].y, + groupId: this.groupId + }); + } + return combinedData; + }; - for (var i = 0; i < nodeIds.length; i++) { - var nodeId = nodeIds[i]; - if (nodes[nodeId] !== undefined) { - if (nodes[nodeId].options.physics === true) { - velocities[nodeId].x = this.previousStates[nodeId].vx; - velocities[nodeId].y = this.previousStates[nodeId].vy; - nodes[nodeId].x = this.previousStates[nodeId].x; - nodes[nodeId].y = this.previousStates[nodeId].y; - } - } else { - delete this.previousStates[nodeId]; + /** + * draw a bar graph + * + * @param groupIds + * @param processedGroupData + */ + Bargraph.draw = function (groupIds, processedGroupData, framework) { + var combinedData = []; + var intersections = {}; + var coreDistance; + var key, drawData; + var group; + var i, j; + var barPoints = 0; + + // combine all barchart data + for (i = 0; i < groupIds.length; i++) { + group = framework.groups[groupIds[i]]; + if (group.options.style === 'bar') { + if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] === true)) { + for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { + combinedData.push({ + x: processedGroupData[groupIds[i]][j].x, + y: processedGroupData[groupIds[i]][j].y, + groupId: groupIds[i], + label: processedGroupData[groupIds[i]][j].label + }); + barPoints += 1; } } } - }, { - key: 'moveNodes', - - /** - * move the nodes one timestap and check if they are stabilized - * @returns {boolean} - */ - value: function moveNodes() { - var nodesPresent = false; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1000000000; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); + } - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - var nodeVelocity = this._performStep(nodeId, maxVelocity); - // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - stabilized = nodeVelocity < vminCorrected && stabilized === true; - nodesPresent = true; - } + if (barPoints === 0) { + return; + } - if (nodesPresent === true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; - } else { - return stabilized; - } - } - return true; + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; + } else { + return a.x - b.x; } - }, { - key: '_performStep', + }); - /** - * Perform the actual step - * - * @param nodeId - * @param maxVelocity - * @returns {number} - * @private - */ - value: function _performStep(nodeId, maxVelocity) { - var node = this.body.nodes[nodeId]; - var timestep = this.options.timestep; - var forces = this.physicsBody.forces; - var velocities = this.physicsBody.velocities; + // get intersections + Bargraph._getDataIntersections(intersections, combinedData); - // store the state so we can revert - this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; + // plot barchart + for (i = 0; i < combinedData.length; i++) { + group = framework.groups[combinedData[i].groupId]; + var minWidth = 0.1 * group.options.barChart.width; - if (node.options.fixed.x === false) { - var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force - var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration - velocities[nodeId].x += ax * timestep; // velocity - velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; - node.x += velocities[nodeId].x * timestep; // position - } else { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; + key = combinedData[i].x; + var heightOffset = 0; + if (intersections[key] === undefined) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - key); } - - if (node.options.fixed.y === false) { - var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force - var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration - velocities[nodeId].y += ay * timestep; // velocity - velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; - node.y += velocities[nodeId].y * timestep; // position - } else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + } else { + var nextKey = i + (intersections[key].amount - intersections[key].resolved); + var prevKey = i - (intersections[key].resolved + 1); + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + intersections[key].resolved += 1; - var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); - return totalVelocity; - } - }, { - key: 'calculateForces', - - /** - * calculate the forces for one physics iteration. - */ - value: function calculateForces() { - this.gravitySolver.solve(); - this.nodesSolver.solve(); - this.edgesSolver.solve(); - } - }, { - key: '_freezeNodes', - - /** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private - */ - value: function _freezeNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x && nodes[id].y) { - this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; - nodes[id].options.fixed.x = true; - nodes[id].options.fixed.y = true; - } + if (group.options.stack === true) { + if (combinedData[i].y < group.zeroPosition) { + heightOffset = intersections[key].accumulatedNegative; + intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y; + } else { + heightOffset = intersections[key].accumulatedPositive; + intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y; + } + } else if (group.options.barChart.sideBySide === true) { + drawData.width = drawData.width / intersections[key].amount; + drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); + if (group.options.barChart.align === 'left') { + drawData.offset -= 0.5 * drawData.width; + } else if (group.options.barChart.align === 'right') { + drawData.offset += 0.5 * drawData.width; } } } - }, { - key: '_restoreFrozenNodes', + DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg); + // draw points + if (group.options.drawPoints.enabled === true) { + Points.draw([combinedData[i]], group, framework, drawData.offset); + //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); + } + } + }; - /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private - */ - value: function _restoreFrozenNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (this.freezeCache[id] !== undefined) { - nodes[id].options.fixed.x = this.freezeCache[id].x; - nodes[id].options.fixed.y = this.freezeCache[id].y; - } - } + /** + * Fill the intersections object with counters of how many datapoints share the same x coordinates + * @param intersections + * @param combinedData + * @private + */ + Bargraph._getDataIntersections = function (intersections, combinedData) { + // get intersections + var coreDistance; + for (var i = 0; i < combinedData.length; i++) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); + } + if (coreDistance === 0) { + if (intersections[combinedData[i].x] === undefined) { + intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; } - this.freezeCache = {}; + intersections[combinedData[i].x].amount += 1; } - }, { - key: 'stabilize', + } + }; - /** - * Find a stable position for all nodes - * @private - */ - value: function stabilize() { - // stop the render loop - this.stopSimulation(); + /** + * Get the width and offset for bargraphs based on the coredistance between datapoints + * + * @param coreDistance + * @param group + * @param minWidth + * @returns {{width: Number, offset: Number}} + * @private + */ + Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { + var width, offset; + if (coreDistance < group.options.barChart.width && coreDistance > 0) { + width = coreDistance < minWidth ? minWidth : coreDistance; - // set stabilze to false - this.stabilized = false; + offset = 0; // recalculate offset with the new width; + if (group.options.barChart.align === 'left') { + offset -= 0.5 * coreDistance; + } else if (group.options.barChart.align === 'right') { + offset += 0.5 * coreDistance; + } + } else { + // default settings + width = group.options.barChart.width; + offset = 0; + if (group.options.barChart.align === 'left') { + offset -= 0.5 * group.options.barChart.width; + } else if (group.options.barChart.align === 'right') { + offset += 0.5 * group.options.barChart.width; + } + } - // block redraw requests - this.body.emitter.emit('_blockRedrawRequests'); - this.body.emitter.emit('startStabilizing'); - this.startedStabilization = true; + return { width: width, offset: offset }; + }; - // start the stabilization - if (this.options.stabilization.onlyDynamicEdges === true) { - this._freezeNodes(); + Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { + if (combinedData.length > 0) { + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; + } else { + return a.x - b.x; } - this.stabilizationIterations = 0; + }); + var intersections = {}; - setTimeout(this._stabilizationBatch.bind(this), 0); - } - }, { - key: '_stabilizationBatch', - value: function _stabilizationBatch() { - var count = 0; - while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.options.stabilization.iterations) { - this.physicsTick(); - this.stabilizationIterations++; - count++; - } + Bargraph._getDataIntersections(intersections, combinedData); + groupRanges[groupLabel] = Bargraph._getStackedYRange(intersections, combinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; - if (this.stabilized === false && this.stabilizationIterations < this.options.stabilization.iterations) { - this.body.emitter.emit('stabilizationProgress', { iterations: this.stabilizationIterations, total: this.options.stabilization.iterations }); - setTimeout(this._stabilizationBatch.bind(this), 0); + Bargraph._getStackedYRange = function (intersections, combinedData) { + var key; + var yMin = combinedData[0].y; + var yMax = combinedData[0].y; + for (var i = 0; i < combinedData.length; i++) { + key = combinedData[i].x; + if (intersections[key] === undefined) { + yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; + yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; + } else { + if (combinedData[i].y < 0) { + intersections[key].accumulatedNegative += combinedData[i].y; } else { - this._finalizeStabilization(); + intersections[key].accumulatedPositive += combinedData[i].y; } } - }, { - key: '_finalizeStabilization', - value: function _finalizeStabilization() { - this.body.emitter.emit('_allowRedrawRequests'); - if (this.options.stabilization.fit === true) { - this.body.emitter.emit('fit'); - } + } + for (var xpos in intersections) { + if (intersections.hasOwnProperty(xpos)) { + yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; + yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; + yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; + yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; + } + } - if (this.options.stabilization.onlyDynamicEdges === true) { - this._restoreFrozenNodes(); - } + return { min: yMin, max: yMax }; + }; - this.body.emitter.emit('stabilizationIterationsDone'); - this.body.emitter.emit('_requestRedraw'); + module.exports = Bargraph; - this.ready = true; - } - }]); +/***/ }, +/* 52 */ +/***/ function(module, exports, __webpack_require__) { - return PhysicsEngine; - })(); + 'use strict'; - exports['default'] = PhysicsEngine; - module.exports = exports['default']; + var DOMutil = __webpack_require__(2); + + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } + + Points.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; + + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); + }; + + /** + * draw the data points + * + * @param {Array} dataset + * @param {Object} JSONcontainer + * @param {Object} svg | SVG DOM element + * @param {GraphGroup} group + * @param {Number} [offset] + */ + Points.draw = function (dataset, group, framework, offset) { + if (offset === undefined) { + offset = 0; + } + for (var i = 0; i < dataset.length; i++) { + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); + } + }; + + module.exports = Points; /***/ }, -/* 54 */ +/* 53 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -20747,645 +20334,608 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var _componentsNodesCluster = __webpack_require__(80); + var util = __webpack_require__(1); - var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); + /** + * @class Groups + * This class can store groups and options specific for groups. + */ - var util = __webpack_require__(1); + var Groups = (function () { + function Groups() { + _classCallCheck(this, Groups); - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; - this.body = body; - this.clusteredNodes = {}; + this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } } // 20:bright red + ]; this.options = {}; - this.defaultOptions = {}; + this.defaultOptions = { + useDefaultGroups: true + }; util.extend(this.options, this.defaultOptions); } - _createClass(ClusterEngine, [{ + _createClass(Groups, [{ key: "setOptions", value: function setOptions(options) { - if (options !== undefined) {} + var optionFields = ["useDefaultGroups"]; + + if (options !== undefined) { + for (var groupName in options) { + if (options.hasOwnProperty(groupName)) { + if (optionFields.indexOf(groupName) === -1) { + var group = options[groupName]; + this.add(groupName, group); + } + } + } + } } }, { - key: "clusterByHubsize", + key: "clear", /** - * - * @param hubsize - * @param options - */ - value: function clusterByHubsize(hubsize, options) { - if (hubsize === undefined) { - hubsize = this._getHubSize(); - } else if (tyepof(hubsize) === "object") { - options = this._checkOptions(hubsize); - hubsize = this._getHubSize(); - } + * Clear all groups + */ + value: function clear() { + this.groups = {}; + this.groupsArray = []; + } + }, { + key: "get", - var nodesToCluster = []; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length >= hubsize) { - nodesToCluster.push(node.id); + /** + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options + */ + value: function get(groupname) { + var group = this.groups[groupname]; + if (group === undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; } } - for (var i = 0; i < nodesToCluster.length; i++) { - var node = this.body.nodes[nodesToCluster[i]]; - this.clusterByConnection(node, options, false); - } - this.body.emitter.emit("_dataChanged"); + return group; + } + }, { + key: "add", + + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + value: function add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; } - }, { - key: "cluster", + }]); - /** - * loop over all nodes, check if they adhere to the condition and cluster if needed. - * @param options - * @param refreshData - */ - value: function cluster() { - var options = arguments[0] === undefined ? {} : arguments[0]; - var refreshData = arguments[1] === undefined ? true : arguments[1]; + return Groups; + })(); - if (options.joinCondition === undefined) { - throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); - } + exports["default"] = Groups; + module.exports = exports["default"]; - // check if the options object is fine, append if needed - options = this._checkOptions(options); +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { - var childNodesObj = {}; - var childEdgesObj = {}; + 'use strict'; - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) === true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - } + Object.defineProperty(exports, '__esModule', { + value: true + }); - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - } - }, { - key: "clusterOutliers", + 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; }; })(); - /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ - value: function clusterOutliers(options) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - options = this._checkOptions(options); - var clusters = []; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var childNodesObj = {}; - var childEdgesObj = {}; - var nodeId = this.body.nodeIndices[i]; - if (this.body.nodes[nodeId].edges.length === 1) { - var edge = this.body.nodes[nodeId].edges[0]; - var childNodeId = this._getConnectedId(edge, nodeId); - if (childNodeId != nodeId) { - if (options.joinCondition === undefined) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) === true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - clonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(clonedOptions) === true) { - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - } - clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); - } - } - } + var _componentsNode = __webpack_require__(73); - for (var i = 0; i < clusters.length; i++) { - this._cluster(clusters[i].nodes, clusters[i].edges, options, false); - } + var _componentsNode2 = _interopRequireDefault(_componentsNode); - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } - } - }, { - key: "clusterByConnection", + var _componentsSharedLabel = __webpack_require__(74); - /** - * suck all connected nodes of a node into the node. - * @param nodeId - * @param options - * @param refreshData - */ - value: function clusterByConnection(nodeId, options) { - var refreshData = arguments[2] === undefined ? true : arguments[2]; + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - // kill conditions - if (nodeId === undefined) { - throw new Error("No nodeId supplied to clusterByConnection!"); - } - if (this.body.nodes[nodeId] === undefined) { - throw new Error("The nodeId given to clusterByConnection does not exist!"); - } + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); - var node = this.body.nodes[nodeId]; - options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) { - options.clusterNodeProperties.x = node.x; - } - if (options.clusterNodeProperties.y === undefined) { - options.clusterNodeProperties.y = node.y; - } - if (options.clusterNodeProperties.fixed === undefined) { - options.clusterNodeProperties.fixed = {}; - options.clusterNodeProperties.fixed.x = node.options.fixed.x; - options.clusterNodeProperties.fixed.y = node.options.fixed.y; - } + var NodesHandler = (function () { + function NodesHandler(body, images, groups, layoutEngine) { + var _this = this; - var childNodesObj = {}; - var childEdgesObj = {}; - var parentNodeId = node.id; - var parentClonedOptions = this._cloneOptions(parentNodeId); - childNodesObj[parentNodeId] = node; + _classCallCheck(this, NodesHandler); - // collect the nodes that will be in the cluster - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - var childNodeId = this._getConnectedId(edge, parentNodeId); + this.body = body; + this.images = images; + this.groups = groups; + this.layoutEngine = layoutEngine; - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + // create the node API in the body container + this.body.functions.createNode = this.create.bind(this); + + this.nodesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items, params.data); + }, + remove: function remove(event, params) { + _this.remove(params.items); + } + }; + + this.options = {}; + this.defaultOptions = { + borderWidth: 1, + borderWidthSelected: undefined, + brokenImage: undefined, + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' + }, + hover: { + border: '#2B7CE9', + background: '#D2E5FF' + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 0, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + group: undefined, + hidden: false, + icon: { + face: 'FontAwesome', //'FontAwesome', + code: undefined, //'\uf007', + size: 50, //50, + color: '#2B7CE9' //'#aa00ff' + }, + image: undefined, // --> URL + label: undefined, + level: undefined, + mass: 1, + physics: true, + scaling: { + min: 10, + max: 30, + label: { + enabled: false, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 + }, + customScalingFunction: function customScalingFunction(min, max, total, value) { + if (max === min) { + return 0.5; } else { - // clone the options and insert some additional parameters that could be interesting. - var childClonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); } - } else { - childEdgesObj[edge.id] = edge; } - } - - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - } - }, { - key: "_cloneOptions", - - /** - * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. - * @param objId - * @param type - * @returns {{}} - * @private - */ - value: function _cloneOptions(objId, type) { - var clonedOptions = {}; - if (type === undefined || type === "node") { - util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); - clonedOptions.x = this.body.nodes[objId].x; - clonedOptions.y = this.body.nodes[objId].y; - clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; - } else { - util.deepExtend(clonedOptions, this.body.edges[objId].options, true); - } - return clonedOptions; - } - }, { - key: "_createClusterEdges", + }, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + shape: 'ellipse', + size: 25, + title: undefined, + value: undefined, + x: undefined, + y: undefined + }; + util.extend(this.options, this.defaultOptions); - /** - * This function creates the edges that will be attached to the cluster. - * - * @param childNodesObj - * @param childEdgesObj - * @param newEdges - * @param options - * @private - */ - value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { - var edge = undefined, - childNodeId = undefined, - childNode = undefined; + this.bindEventListeners(); + } - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; + _createClass(NodesHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this2 = this; - // mark all edges for removal from global and construct new edges from the cluster to others - for (var j = 0; j < childNode.edges.length; j++) { - edge = childNode.edges[j]; - childEdgesObj[edge.id] = edge; + // refresh the nodes. Used when reverting from hierarchical layout + this.body.emitter.on('refreshNodes', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); + this.body.emitter.on('destroy', function () { + delete _this2.body.functions.createNode; + delete _this2.nodesListeners.add; + delete _this2.nodesListeners.update; + delete _this2.nodesListeners.remove; + delete _this2.nodesListeners; + }); + } + }, { + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + _componentsNode2['default'].parseOptions(this.options, options); - var otherNodeId = edge.toId; - var otherOnTo = true; - if (edge.toId != childNodeId) { - otherNodeId = edge.toId; - otherOnTo = true; - } else if (edge.fromId != childNodeId) { - otherNodeId = edge.fromId; - otherOnTo = false; + // update the shape in all nodes + if (options.shape !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateShape(); + } } + } - if (childNodesObj[otherNodeId] === undefined) { - var clonedOptions = this._cloneOptions(edge.id, "edge"); - util.deepExtend(clonedOptions, options.clusterEdgeProperties); - if (otherOnTo === true) { - clonedOptions.from = options.clusterNodeProperties.id; - clonedOptions.to = otherNodeId; - } else { - clonedOptions.from = otherNodeId; - clonedOptions.to = options.clusterNodeProperties.id; + // update the shape size in all nodes + if (options.font !== undefined) { + _componentsSharedLabel2['default'].parseOptions(this.options.font, options); + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateLabelModule(); + this.body.nodes[nodeId]._reset(); } - clonedOptions.id = "clusterEdge:" + util.randomUUID(); - newEdges.push(this.body.functions.createEdge(clonedOptions)); } } - } - } - }, { - key: "_checkOptions", - /** - * This function checks the options that can be supplied to the different cluster functions - * for certain fields and inserts defaults if needed - * @param options - * @returns {*} - * @private - */ - value: function _checkOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; + // update the shape size in all nodes + if (options.size !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId]._reset(); + } + } + } - if (options.clusterEdgeProperties === undefined) { - options.clusterEdgeProperties = {}; - } - if (options.clusterNodeProperties === undefined) { - options.clusterNodeProperties = {}; + // update the state of the letiables if needed + if (options.hidden !== undefined || options.physics !== undefined) { + this.body.emitter.emit('_dataChanged'); + } } - - return options; } }, { - key: "_cluster", + key: 'setData', /** - * - * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node - * @param {Object} childEdgesObj | object with edge objects, id as keys - * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} - * @param {Boolean} refreshData | when true, do not wrap up - * @private - */ - value: function _cluster(childNodesObj, childEdgesObj, options) { - var refreshData = arguments[3] === undefined ? true : arguments[3]; - - // kill condition: no children so cant cluster - if (Object.keys(childNodesObj).length === 0) { - return; - } - - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); - } - var clusterId = options.clusterNodeProperties.id; + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ + value: function setData(nodes) { + var _this3 = this; - // construct the clusterNodeProperties - var clusterNodeProperties = options.clusterNodeProperties; - if (options.processProperties !== undefined) { - // get the childNode options - var childNodesOptions = []; - for (var nodeId in childNodesObj) { - var clonedOptions = this._cloneOptions(nodeId); - childNodesOptions.push(clonedOptions); - } + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - // get clusterproperties based on childNodes - var childEdgesOptions = []; - for (var edgeId in childEdgesObj) { - var clonedOptions = this._cloneOptions(edgeId, "edge"); - childEdgesOptions.push(clonedOptions); - } + var oldNodesData = this.body.data.nodes; - clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); - if (!clusterNodeProperties) { - throw new Error("The processClusterProperties function does not return properties!"); - } - } - if (clusterNodeProperties.label === undefined) { - clusterNodeProperties.label = "cluster"; + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.body.data.nodes = nodes; + } else if (Array.isArray(nodes)) { + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); + } else if (!nodes) { + this.body.data.nodes = new DataSet(); + } else { + throw new TypeError('Array or DataSet expected'); } - // give the clusterNode a postion if it does not have one. - var pos = undefined; - if (clusterNodeProperties.x === undefined) { - pos = this._getClusterPosition(childNodesObj); - clusterNodeProperties.x = pos.x; - } - if (clusterNodeProperties.y === undefined) { - if (pos === undefined) { - pos = this._getClusterPosition(childNodesObj); - } - clusterNodeProperties.y = pos.y; + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); } - // force the ID to remain the same - clusterNodeProperties.id = clusterId; - - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties, _componentsNodesCluster2["default"]); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; - - // finally put the cluster node into global - this.body.nodes[clusterNodeProperties.id] = clusterNode; + // remove drawn nodes + this.body.nodes = {}; - // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + if (this.body.data.nodes) { + (function () { + // subscribe to new dataset + var me = _this3; + util.forEach(_this3.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); - // disable the childEdges - for (var edgeId in childEdgesObj) { - if (childEdgesObj.hasOwnProperty(edgeId)) { - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - edge.togglePhysics(false); - edge.options.hidden = true; - } - } + // draw all new nodes + var ids = _this3.body.data.nodes.getIds(); + _this3.add(ids, true); + })(); } - // disable the childNodes - for (var nodeId in childNodesObj) { - if (childNodesObj.hasOwnProperty(nodeId)) { - this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; - this.body.nodes[nodeId].togglePhysics(false); - this.body.nodes[nodeId].options.hidden = true; - } + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } + } + }, { + key: 'add', - // push new edges to global - for (var i = 0; i < newEdges.length; i++) { - this.body.edges[newEdges[i].id] = newEdges[i]; - this.body.edges[newEdges[i].id].connect(); + /** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + + var id = undefined; + var newNodes = []; + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + var _properties = this.body.data.nodes.get(id); + var node = this.create(_properties); + newNodes.push(node); + this.body.nodes[id] = node; // note: this may replace an existing node } - // set ID to undefined so no duplicates arise - clusterNodeProperties.id = undefined; + this.layoutEngine.positionInitially(newNodes); - // wrap up - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } } }, { - key: "isCluster", + key: 'update', /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} - */ - value: function isCluster(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].isCluster === true; - } else { - console.log("Node does not exist."); - return false; + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids, changedData) { + var nodes = this.body.nodes; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node !== undefined) { + // update node + node.setOptions(data, this.constants); + } else { + dataChanged = true; + // create node + node = this.create(properties); + nodes[id] = node; + } } - } - }, { - key: "_getClusterPosition", - /** - * get the position of the cluster node based on what's inside - * @param {object} childNodesObj | object with node objects, id as keys - * @returns {{x: number, y: number}} - * @private - */ - value: function _getClusterPosition(childNodesObj) { - var childKeys = Object.keys(childNodesObj); - var minX = childNodesObj[childKeys[0]].x; - var maxX = childNodesObj[childKeys[0]].x; - var minY = childNodesObj[childKeys[0]].y; - var maxY = childNodesObj[childKeys[0]].y; - var node = undefined; - for (var i = 0; i < childKeys.lenght; i++) { - node = childNodesObj[childKeys[0]]; - minX = node.x < minX ? node.x : minX; - maxX = node.x > maxX ? node.x : maxX; - minY = node.y < minY ? node.y : minY; - maxY = node.y > maxY ? node.y : maxY; + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); + } else { + this.body.emitter.emit('_dataUpdated'); } - return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; } }, { - key: "openCluster", + key: 'remove', /** - * Open a cluster by calling this function. - * @param {String} clusterNodeId | the ID of the cluster node - * @param {Boolean} refreshData | wrap up afterwards if not true - */ - value: function openCluster(clusterNodeId) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var nodes = this.body.nodes; - // kill conditions - if (clusterNodeId === undefined) { - throw new Error("No clusterNodeId supplied to openCluster."); - } - if (this.body.nodes[clusterNodeId] === undefined) { - throw new Error("The clusterNodeId supplied to openCluster does not exist."); - } - if (this.body.nodes[clusterNodeId].containedNodes === undefined) { - console.log("The node:" + clusterNodeId + " is not a cluster."); - return; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + delete nodes[id]; } - var clusterNode = this.body.nodes[clusterNodeId]; - var containedNodes = clusterNode.containedNodes; - var containedEdges = clusterNode.containedEdges; - - // release nodes - for (var nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - var containedNode = this.body.nodes[nodeId]; - containedNode = containedNodes[nodeId]; - // inherit position - containedNode.x = clusterNode.x; - containedNode.y = clusterNode.y; - // inherit speed - containedNode.vx = clusterNode.vx; - containedNode.vy = clusterNode.vy; + this.body.emitter.emit('_dataChanged'); + } + }, { + key: 'create', - containedNode.options.hidden = false; - containedNode.togglePhysics(true); + /** + * create a node + * @param properties + * @param constructorClass + */ + value: function create(properties) { + var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; - delete this.clusteredNodes[nodeId]; + return new constructorClass(properties, this.body, this.images, this.groups, this.options); + } + }, { + key: 'refresh', + value: function refresh() { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + var node = undefined; + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; } - } - - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = this.body.edges[edgeId]; - edge.options.hidden = false; - edge.togglePhysics(true); + var data = this.body.data.nodes._data[nodeId]; + if (node !== undefined && data !== undefined) { + node.setOptions({ fixed: false }); + node.setOptions(data); } } - - // remove all temporary edges - for (var i = 0; i < clusterNode.edges.length; i++) { - var edgeId = clusterNode.edges[i].id; - this.body.edges[edgeId].edgeType.cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; - } - - // remove clusterNode - delete this.body.nodes[clusterNodeId]; - - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } } }, { - key: "_connectEdge", + key: 'getPositions', /** - * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to - * is currently residing in cluster B - * @param edge - * @param nodeId - * @param from - * @private - */ - value: function _connectEdge(edge, nodeId, from) { - var clusterStack = this.findNode(nodeId); - if (from === true) { - edge.from = clusterStack[clusterStack.length - 1]; - edge.fromId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.fromArray = clusterStack; + * Returns the positions of the nodes. + * @param ids --> optional, can be array of nodeIds, can be string + * @returns {{}} + */ + value: function getPositions(ids) { + var dataArray = {}; + if (ids !== undefined) { + if (Array.isArray(ids) === true) { + for (var i = 0; i < ids.length; i++) { + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; + dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; + dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } } else { - edge.to = clusterStack[clusterStack.length - 1]; - edge.toId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.toArray = clusterStack; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } } - edge.connect(); + return dataArray; } }, { - key: "findNode", + key: 'storePositions', /** - * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node - * @param nodeId - * @returns {Array} - * @private - */ - value: function findNode(nodeId) { - var stack = []; - var max = 100; - var counter = 0; + * Load the XY positions of the nodes into the dataset. + */ + value: function storePositions() { + // todo: add support for clusters and hierarchical. + var dataArray = []; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { + dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); + } + } + } + this.body.data.nodes.update(dataArray); + } + }, { + key: 'getBoundingBox', - while (this.clusteredNodes[nodeId] !== undefined && counter < max) { - stack.push(this.clusteredNodes[nodeId].node); - nodeId = this.clusteredNodes[nodeId].clusterId; - counter++; + /** + * get the bounding box of a node. + * @param nodeId + * @returns {j|*} + */ + value: function getBoundingBox(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].shape.boundingBox; } - stack.push(this.body.nodes[nodeId]); - return stack; } }, { - key: "_getConnectedId", + key: 'getConnectedNodes', /** - * Get the Id the node is connected to - * @param edge - * @param nodeId - * @returns {*} - * @private - */ - value: function _getConnectedId(edge, nodeId) { - if (edge.toId != nodeId) { - return edge.toId; - } else if (edge.fromId != nodeId) { - return edge.fromId; - } else { - return edge.fromId; + * Get the Ids of nodes connected to this node. + * @param nodeId + * @returns {Array} + */ + value: function getConnectedNodes(nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = {}; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId === nodeId) { + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; + } + } else if (edge.fromId === nodeId) { + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; + } + } + } } + return nodeList; } }, { - key: "_getHubSize", + key: 'getEdges', /** - * We determine how many connections denote an important hub. - * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) - * - * @private - */ - value: function _getHubSize() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; - - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length > largestHub) { - largestHub = node.edges.length; + * Get the ids of the edges connected to this node. + * @param nodeId + * @returns {*} + */ + value: function getEdges(nodeId) { + var edgeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgeList.push(node.edges[i].id); } - average += node.edges.length; - averageSquared += Math.pow(node.edges.length, 2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; - - var letiance = averageSquared - Math.pow(average, 2); - var standardDeviation = Math.sqrt(letiance); - - var hubThreshold = Math.floor(average + 2 * standardDeviation); - - // always have at least one to cluster - if (hubThreshold > largestHub) { - hubThreshold = largestHub; } - - return hubThreshold; + return nodeList; } }]); - return ClusterEngine; + return NodesHandler; })(); - exports["default"] = ClusterEngine; - module.exports = exports["default"]; + exports['default'] = NodesHandler; + module.exports = exports['default']; /***/ }, /* 55 */ @@ -21399,330 +20949,412 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - if (typeof window !== 'undefined') { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } + var _componentsEdge = __webpack_require__(75); + + var _componentsEdge2 = _interopRequireDefault(_componentsEdge); + + var _componentsSharedLabel = __webpack_require__(74); + + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); - var CanvasRenderer = (function () { - function CanvasRenderer(body, canvas) { - _classCallCheck(this, CanvasRenderer); + var EdgesHandler = (function () { + function EdgesHandler(body, images, groups) { + var _this = this; + + _classCallCheck(this, EdgesHandler); this.body = body; - this.canvas = canvas; + this.images = images; + this.groups = groups; - this.redrawRequested = false; - this.renderTimer = undefined; - this.requiresTimeout = true; - this.renderingActive = false; - this.renderRequests = 0; - this.pixelRatio = undefined; - this.allowRedrawRequests = true; + // create the edge API in the body container + this.body.functions.createEdge = this.create.bind(this); + + this.edgesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items); + }, + remove: function remove(event, params) { + _this.remove(params.items); + } + }; - this.dragging = false; this.options = {}; this.defaultOptions = { - hideEdgesOnDrag: false, - hideNodesOnDrag: false + arrows: { + to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: 1 }, + from: { enabled: false, scaleFactor: 1 } + }, + color: { + color: '#848484', + highlight: '#848484', + hover: '#848484', + inherit: 'from', + opacity: 1 + }, + dashes: false, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 2, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + hidden: false, + hoverWidth: 1.5, + label: undefined, + length: undefined, + physics: true, + scaling: { + min: 1, + max: 15, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 + }, + customScalingFunction: function customScalingFunction(min, max, total, value) { + if (max === min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + selectionWidth: 1, + selfReferenceSize: 20, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + smooth: { + enabled: true, + dynamic: true, + type: 'continuous', + roundness: 0.5 + }, + title: undefined, + width: 1, + value: undefined }; + util.extend(this.options, this.defaultOptions); - this._determineBrowserMethod(); this.bindEventListeners(); } - _createClass(CanvasRenderer, [{ + _createClass(EdgesHandler, [{ key: 'bindEventListeners', value: function bindEventListeners() { - var _this = this; + var _this2 = this; - this.body.emitter.on('dragStart', function () { - _this.dragging = true; - }); - this.body.emitter.on('dragEnd', function () { - return _this.dragging = false; - }); - this.body.emitter.on('_redraw', function () { - if (_this.renderingActive === false) { - _this._redraw(); + // this allows external modules to force all dynamic curves to turn static. + this.body.emitter.on('_forceDisableDynamicCurves', function (type) { + var emitChange = false; + for (var edgeId in _this2.body.edges) { + if (_this2.body.edges.hasOwnProperty(edgeId)) { + var edge = _this2.body.edges[edgeId]; + var edgeData = _this2.body.data.edges._data[edgeId]; + + // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. + // this is because a change in the global would not affect these curves. + if (edgeData !== undefined) { + var edgeOptions = edgeData.smooth; + if (edgeOptions !== undefined) { + if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { + if (type === undefined) { + edge.setOptions({ smooth: false }); + } else { + edge.setOptions({ smooth: { dynamic: false, type: type } }); + } + emitChange = true; + } + } + } + } + } + if (emitChange === true) { + _this2.body.emitter.emit('_dataChanged'); } }); - this.body.emitter.on('_blockRedrawRequests', function () { - _this.allowRedrawRequests = false; - }); - this.body.emitter.on('_allowRedrawRequests', function () { - _this.allowRedrawRequests = true; - }); - this.body.emitter.on('_requestRedraw', this._requestRedraw.bind(this)); - this.body.emitter.on('_startRendering', function () { - _this.renderRequests += 1; - _this.renderingActive = true; - _this._startRendering(); - }); - this.body.emitter.on('_stopRendering', function () { - _this.renderRequests -= 1; - _this.renderingActive = _this.renderRequests > 0; - _this.renderTimer = undefined; + + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on('_dataUpdated', function () { + _this2.reconnectEdges(); + _this2.markAllEdgesAsDirty(); }); + + // refresh the edges. Used when reverting from hierarchical layout + this.body.emitter.on('refreshEdges', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); this.body.emitter.on('destroy', function () { - _this.renderRequests = 0; - _this.renderingActive = false; - if (_this.requiresTimeout === true) { - clearTimeout(_this.renderTimer); - } else { - cancelAnimationFrame(_this.renderTimer); - } - _this.body.emitter.off(); + delete _this2.body.functions.createEdge; + delete _this2.edgesListeners.add; + delete _this2.edgesListeners.update; + delete _this2.edgesListeners.remove; + delete _this2.edgesListeners; }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { - var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag']; - util.selectiveDeepExtend(fields, this.options, options); - } - } - }, { - key: '_startRendering', - value: function _startRendering() { - if (this.renderingActive === true) { - if (this.renderTimer === undefined) { - if (this.requiresTimeout === true) { - this.renderTimer = window.setTimeout(this._renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function - } else { - this.renderTimer = window.requestAnimationFrame(this._renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function - } + // use the parser from the Edge class to fill in all shorthand notations + _componentsEdge2['default'].parseOptions(this.options, options); + + // hanlde multiple input cases for color + if (options.color !== undefined) { + this.markAllEdgesAsDirty(); } - } - } - }, { - key: '_renderStep', - value: function _renderStep() { - if (this.renderingActive === true) { - // reset the renderTimer so a new scheduled animation step can be set - this.renderTimer = undefined; - if (this.requiresTimeout === true) { - // this schedules a new simulation step - this._startRendering(); + // update smooth settings in all edges + var dataChanged = false; + if (options.smooth !== undefined) { + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; + } + } } - this._redraw(); + // update fonts in all edges + if (options.font !== undefined) { + // use the parser from the Label class to fill in all shorthand notations + _componentsSharedLabel2['default'].parseOptions(this.options, options); + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].updateLabelModule(); + } + } + } - if (this.requiresTimeout === false) { - // this schedules a new simulation step - this._startRendering(); + // update the state of the variables if needed + if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { + this.body.emitter.emit('_dataChanged'); } } } }, { - key: 'redraw', - - /** - * Redraw the network with the current data - * chart will be resized too. - */ - value: function redraw() { - this.body.emitter.emit('setSize'); - this._redraw(); - } - }, { - key: '_requestRedraw', + key: 'setData', /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private * @private */ - value: function _requestRedraw() { - if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedrawRequests === true) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, false), 0); - } else { - window.requestAnimationFrame(this._redraw.bind(this, false)); - } - } - } - }, { - key: '_redraw', - value: function _redraw() { - var hidden = arguments[0] === undefined ? false : arguments[0]; + value: function setData(edges) { + var _this3 = this; - this.body.emitter.emit('initRedraw'); + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - this.redrawRequested = false; - var ctx = this.canvas.frame.canvas.getContext('2d'); + var oldEdgesData = this.body.data.edges; - // when the container div was hidden, this fixes it back up! - if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) { - this.canvas.setSize(); + if (edges instanceof DataSet || edges instanceof DataView) { + this.body.data.edges = edges; + } else if (Array.isArray(edges)) { + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); + } else if (!edges) { + this.body.data.edges = new DataSet(); + } else { + throw new TypeError('Array or DataSet expected'); } - if (this.pixelRatio === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + // TODO: is this null or undefined or false? + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.canvas.frame.canvas.clientWidth; - var h = this.canvas.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); - - this.body.emitter.emit('beforeDrawing', ctx); + // remove drawn edges + this.body.edges = {}; - // set scaling and translation - ctx.save(); - ctx.translate(this.body.view.translation.x, this.body.view.translation.y); - ctx.scale(this.body.view.scale, this.body.view.scale); + // TODO: is this null or undefined or false? + if (this.body.data.edges) { + // subscribe to new dataset + util.forEach(this.edgesListeners, function (callback, event) { + _this3.body.data.edges.on(event, callback); + }); - if (hidden === false) { - if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { - this._drawEdges(ctx); - } + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this.add(ids, true); } - if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { - this._drawNodes(ctx, hidden); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } + } + }, { + key: 'add', - if (this.controlNodesActive === true) { - this._drawControlNodes(ctx); - } + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - //this.physics.nodesSolver._debug(ctx,"#F00F0F"); + var edges = this.body.edges; + var edgesData = this.body.data.edges; - this.body.emitter.emit('afterDrawing', ctx); + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; - // restore original scaling and translation - ctx.restore(); + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } - if (hidden === true) { - ctx.clearRect(0, 0, w, h); + var data = edgesData.get(id, { 'showInternalIds': true }); + edges[id] = this.create(data); + } + + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } } }, { - key: '_drawNodes', + key: 'update', /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids * @private */ - value: function _drawNodes(ctx) { - var alwaysShow = arguments[1] === undefined ? false : arguments[1]; - - var nodes = this.body.nodes; - var nodeIndices = this.body.nodeIndices; - var node = undefined; - var selected = []; - var margin = 20; - var topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin }); - var bottomRight = this.canvas.DOMtoCanvas({ - x: this.canvas.frame.canvas.clientWidth + margin, - y: this.canvas.frame.canvas.clientHeight + margin - }); - var viewableArea = { top: topLeft.y, left: topLeft.x, bottom: bottomRight.y, right: bottomRight.x }; - - // draw unselected nodes; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - // set selected nodes aside - if (node.isSelected()) { - selected.push(nodeIndices[i]); - } else { - if (alwaysShow === true) { - node.draw(ctx); - } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) { - node.draw(ctx); - } + value: function update(ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var data = edgesData.get(id); + var edge = edges[id]; + if (edge === null) { + // update edge + edge.disconnect(); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. + edge.connect(); + } else { + // create edge + this.body.edges[id] = this.create(data); + dataChanged = true; } } - // draw the selected nodes on top - for (var i = 0; i < selected.length; i++) { - node = nodes[selected[i]]; - node.draw(ctx); + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); + } else { + this.body.emitter.emit('_dataUpdated'); } } }, { - key: '_drawEdges', + key: 'remove', /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids * @private */ - value: function _drawEdges(ctx) { + value: function remove(ids) { var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge = undefined; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge !== undefined) { + if (edge.via != null) { + delete this.body.supportNodes[edge.via.id]; + } + edge.disconnect(); + delete edges[id]; + } + } - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edge.draw(ctx); + this.body.emitter.emit('_dataChanged'); + } + }, { + key: 'refresh', + value: function refresh() { + var edges = this.body.edges; + for (var edgeId in edges) { + var edge = undefined; + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + } + var data = this.body.data.edges._data[edgeId]; + if (edge !== undefined && data !== undefined) { + edge.setOptions(data); } } } }, { - key: '_drawControlNodes', + key: 'create', + value: function create(properties) { + return new _componentsEdge2['default'](properties, this.body, this.options); + } + }, { + key: 'markAllEdgesAsDirty', + value: function markAllEdgesAsDirty() { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].edgeType.colorDirty = true; + } + } + }, { + key: 'reconnectEdges', /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * Reconnect all edges * @private */ - value: function _drawControlNodes(ctx) { + value: function reconnectEdges() { + var id; + var nodes = this.body.nodes; var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge = undefined; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - edge._drawControlNodes(ctx); + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } } - } - }, { - key: '_determineBrowserMethod', - /** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame - * @private - */ - value: function _determineBrowserMethod() { - if (typeof window !== 'undefined') { - var browserType = navigator.userAgent.toLowerCase(); - this.requiresTimeout = false; - if (browserType.indexOf('msie 9.0') != -1) { - // IE 9 - this.requiresTimeout = true; - } else if (browserType.indexOf('safari') != -1) { - // safari - if (browserType.indexOf('chrome') <= -1) { - this.requiresTimeout = true; - } + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); } - } else { - this.requiresTimeout = true; } } }]); - return CanvasRenderer; + return EdgesHandler; })(); - exports['default'] = CanvasRenderer; + exports['default'] = EdgesHandler; module.exports = exports['default']; /***/ }, @@ -21737,360 +21369,568 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(45); + var _componentsPhysicsBarnesHutSolver = __webpack_require__(76); - var util = __webpack_require__(1); + var _componentsPhysicsBarnesHutSolver2 = _interopRequireDefault(_componentsPhysicsBarnesHutSolver); - /** - * Create the main frame for the Network. - * This function is executed once when a Network object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private - */ + var _componentsPhysicsRepulsionSolver = __webpack_require__(77); - var Canvas = (function () { - function Canvas(body) { - _classCallCheck(this, Canvas); + var _componentsPhysicsRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsRepulsionSolver); + + var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(78); + + var _componentsPhysicsHierarchicalRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalRepulsionSolver); + + var _componentsPhysicsSpringSolver = __webpack_require__(79); + + var _componentsPhysicsSpringSolver2 = _interopRequireDefault(_componentsPhysicsSpringSolver); + + var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(80); + + var _componentsPhysicsHierarchicalSpringSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalSpringSolver); + + var _componentsPhysicsCentralGravitySolver = __webpack_require__(81); + + var _componentsPhysicsCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsCentralGravitySolver); + + var util = __webpack_require__(1); + + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + _classCallCheck(this, PhysicsEngine); this.body = body; - this.pixelRatio = 1; - this.resizeTimer = undefined; - this.resizeFunction = this._onResize.bind(this); + this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + + this.physicsEnabled = true; + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.freezeCache = {}; + this.renderTimer = undefined; + this.stabilized = false; + this.stabilizationIterations = 0; + this.ready = false; // will be set to true if the stabilize + + // default options this.options = {}; this.defaultOptions = { - autoResize: true, - height: '100%', - width: '100%' + barnesHut: { + theta: 0.5, + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0.2, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 120, + damping: 0.09 + }, + maxVelocity: 50, + minVelocity: 0.1, // px/s + solver: 'barnesHut', + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + fit: true + }, + timestep: 0.5 }; util.extend(this.options, this.defaultOptions); this.bindEventListeners(); } - _createClass(Canvas, [{ + _createClass(PhysicsEngine, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; - // bind the events - this.body.emitter.once('resize', function (obj) { - if (obj.width !== 0) { - _this.body.view.translation.x = obj.width * 0.5; + this.body.emitter.on('initPhysics', function () { + _this.initPhysics(); + }); + this.body.emitter.on('resetPhysics', function () { + _this.stopSimulation();_this.ready = false; + }); + this.body.emitter.on('disablePhysics', function () { + _this.physicsEnabled = false;_this.stopSimulation(); + }); + this.body.emitter.on('restorePhysics', function () { + _this.setOptions(_this.options); + if (_this.ready === true) { + _this.startSimulation(); } - if (obj.height !== 0) { - _this.body.view.translation.y = obj.height * 0.5; + }); + this.body.emitter.on('startSimulation', function () { + if (_this.ready === true) { + _this.startSimulation(); } }); - this.body.emitter.on('setSize', this.setSize.bind(this)); + this.body.emitter.on('stopSimulation', function () { + _this.stopSimulation(); + }); this.body.emitter.on('destroy', function () { - _this.hammerFrame.destroy(); - _this.hammer.destroy(); - _this._cleanUp(); + _this.stopSimulation(false); + _this.body.emitter.off(); }); } }, { key: 'setOptions', value: function setOptions(options) { - var _this2 = this; - if (options !== undefined) { - var fields = ['width', 'height', 'autoResize']; - util.selectiveDeepExtend(fields, this.options, options); - } - - if (this.options.autoResize === true) { - // automatically adapt to a changing size of the browser. - this._cleanUp(); - this.resizeTimer = setInterval(function () { - _this2.setSize();_this2.body.emitter.emit('_requestRedraw'); - }, 1000); - this.resizeFunction = this._onResize.bind(this); - util.addEventListener(window, 'resize', this.resizeFunction); + if (options === false) { + this.physicsEnabled = false; + this.stopSimulation(); + } else { + this.physicsEnabled = true; + util.selectiveNotDeepExtend(['stabilization'], this.options, options); + util.mergeOptions(this.options, options, 'stabilization'); + } } + + this.init(); } }, { - key: '_cleanUp', - value: function _cleanUp() { - // automatically adapt to a changing size of the browser. - if (this.resizeTimer !== undefined) { - clearInterval(this.resizeTimer); + key: 'init', + value: function init() { + var options; + if (this.options.solver === 'repulsion') { + options = this.options.repulsion; + this.nodesSolver = new _componentsPhysicsRepulsionSolver2['default'](this.body, this.physicsBody, options); + this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); + } else if (this.options.solver === 'hierarchicalRepulsion') { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new _componentsPhysicsHierarchicalRepulsionSolver2['default'](this.body, this.physicsBody, options); + this.edgesSolver = new _componentsPhysicsHierarchicalSpringSolver2['default'](this.body, this.physicsBody, options); + } else { + // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new _componentsPhysicsBarnesHutSolver2['default'](this.body, this.physicsBody, options); + this.edgesSolver = new _componentsPhysicsSpringSolver2['default'](this.body, this.physicsBody, options); } - util.removeEventListener(window, 'resize', this.resizeFunction); - this.resizeFunction = undefined; + + this.gravitySolver = new _componentsPhysicsCentralGravitySolver2['default'](this.body, this.physicsBody, options); + this.modelOptions = options; } }, { - key: '_onResize', - value: function _onResize() { - this.setSize(); - this.body.emitter.emit('_redraw'); + key: 'initPhysics', + value: function initPhysics() { + if (this.physicsEnabled === true) { + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.stabilized = false; + this.ready = true; + this.body.emitter.emit('fit', {}, true); + this.startSimulation(); + } + } else { + this.ready = true; + this.body.emitter.emit('fit'); + } } }, { - key: '_prepareValue', - value: function _prepareValue(value) { - if (typeof value === 'number') { - return value + 'px'; - } else if (typeof value === 'string') { - if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) { - return value; - } else if (value.indexOf('%') === -1) { - return value + 'px'; + key: 'startSimulation', + + /** + * Start the simulation + */ + value: function startSimulation() { + if (this.physicsEnabled === true) { + this.stabilized = false; + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on('initRedraw', this.viewFunction); + this.body.emitter.emit('_startRendering'); } + } else { + this.body.emitter.emit('_redraw'); } - throw new Error('Could not use the value supplie for width or height:' + value); } }, { - key: '_create', + key: 'stopSimulation', /** - * Create the HTML + * Stop the simulation, force stabilization. */ - value: function _create() { - // remove all elements from the container element. - while (this.body.container.hasChildNodes()) { - this.body.container.removeChild(this.body.container.firstChild); + value: function stopSimulation() { + var emit = arguments[0] === undefined ? true : arguments[0]; + + this.stabilized = true; + if (emit === true) { + this._emitStabilized(); + } + if (this.viewFunction !== undefined) { + this.body.emitter.off('initRedraw', this.viewFunction); + this.viewFunction = undefined; + if (emit === true) { + this.body.emitter.emit('_stopRendering'); + } } + } + }, { + key: 'simulationStep', - this.frame = document.createElement('div'); - this.frame.className = 'vis-network'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; - this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window + /** + * The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized. + * + */ + value: function simulationStep() { + // check if the physics have settled + var startTime = Date.now(); + this.physicsTick(); + var physicsTime = Date.now() - startTime; - ////////////////////////////////////////////////////////////////// + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed === true) && this.stabilized === false) { + this.physicsTick(); - this.frame.canvas = document.createElement('canvas'); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement('DIV'); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold'; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } else { - var ctx = this.frame.canvas.getContext('2d'); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the 'stabilized' event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + this.stabilizationIterations = 0; + this.startedStabilization = false; + this._emitStabilized(); + } else { + this.stabilizationIterations = 0; + } + this.stopSimulation(); + } + } + }, { + key: '_emitStabilized', + value: function _emitStabilized() { + var _this2 = this; - this.frame.canvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + if (this.stabilizationIterations > 1) { + setTimeout(function () { + _this2.body.emitter.emit('stabilized', { iterations: _this2.stabilizationIterations }); + }, 0); } + } + }, { + key: 'physicsTick', - // add the frame to the container element - this.body.container.appendChild(this.frame); + /** + * A single simulation step (or 'tick') in the physics simulation + * + * @private + */ + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); - this.body.view.scale = 1; - this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization === false) { + this.body.emitter.emit('startStabilizing'); + this.startedStabilization = true; + } + } - this._bindHammer(); + this.stabilizationIterations++; + } } }, { - key: '_bindHammer', + key: 'updatePhysicsIndices', /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time. + * * @private */ - value: function _bindHammer() { - var _this3 = this; + value: function updatePhysicsIndices() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + var nodes = this.body.nodes; + var edges = this.body.edges; - if (this.hammer !== undefined) { - this.hammer.destroy(); + // get node indices for physics + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.physics === true) { + this.physicsBody.physicsNodeIndices.push(nodeId); + } + } } - this.drag = {}; - this.pinch = {}; - - // init hammer - this.hammer = new Hammer(this.frame.canvas); - this.hammer.get('pinch').set({ enable: true }); - // enable to get better response, todo: test on mobile. - //this.hammer.get('pan').set({threshold:2}); - hammerUtil.onTouch(this.hammer, function (event) { - _this3.body.eventListeners.onTouch(event); - }); - this.hammer.on('tap', function (event) { - _this3.body.eventListeners.onTap(event); - }); - this.hammer.on('doubletap', function (event) { - _this3.body.eventListeners.onDoubleTap(event); - }); - this.hammer.on('press', function (event) { - _this3.body.eventListeners.onHold(event); - }); - this.hammer.on('panstart', function (event) { - _this3.body.eventListeners.onDragStart(event); - }); - this.hammer.on('panmove', function (event) { - _this3.body.eventListeners.onDrag(event); - }); - this.hammer.on('panend', function (event) { - _this3.body.eventListeners.onDragEnd(event); - }); - this.hammer.on('pinch', function (event) { - _this3.body.eventListeners.onPinch(event); - }); + // get edge indices for physics + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.physics === true) { + this.physicsBody.physicsEdgeIndices.push(edgeId); + } + } + } - // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? - this.frame.canvas.addEventListener('mousewheel', function (event) { - _this3.body.eventListeners.onMouseWheel(event); - }); - this.frame.canvas.addEventListener('DOMMouseScroll', function (event) { - _this3.body.eventListeners.onMouseWheel(event); - }); + // get the velocity and the forces vector + for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { + var nodeId = this.physicsBody.physicsNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - this.frame.canvas.addEventListener('mousemove', function (event) { - _this3.body.eventListeners.onMouseMove(event); - }); - this.frame.canvas.addEventListener('contextmenu', function (event) { - _this3.body.eventListeners.onContext(event); - }); + // forces can be reset because they are recalculated. Velocities have to persist. + if (this.physicsBody.velocities[nodeId] === undefined) { + this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; + } + } - this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, function (event) { - _this3.body.eventListeners.onRelease(event); - }); + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (nodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } } }, { - key: 'setSize', + key: 'revert', /** - * Set a new size for the network - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') + * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized. */ - value: function setSize() { - var width = arguments[0] === undefined ? this.options.width : arguments[0]; - var height = arguments[1] === undefined ? this.options.height : arguments[1]; + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.body.nodes; + var velocities = this.physicsBody.velocities; - width = this._prepareValue(width); - height = this._prepareValue(height); + for (var i = 0; i < nodeIds.length; i++) { + var nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + if (nodes[nodeId].options.physics === true) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } + } else { + delete this.previousStates[nodeId]; + } + } + } + }, { + key: 'moveNodes', - var emitEvent = false; - var oldWidth = this.frame.canvas.width; - var oldHeight = this.frame.canvas.height; + /** + * move the nodes one timestap and check if they are stabilized + * @returns {boolean} + */ + value: function moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1000000000; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); - if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { - this.frame.style.width = width; - this.frame.style.height = height; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + var nodeVelocity = this._performStep(nodeId, maxVelocity); + // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized + stabilized = nodeVelocity < vminCorrected && stabilized === true; + nodesPresent = true; + } - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + if (nodesPresent === true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; + } else { + return stabilized; + } + } + return true; + } + }, { + key: '_performStep', - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + /** + * Perform the actual step + * + * @param nodeId + * @param maxVelocity + * @returns {number} + * @private + */ + value: function _performStep(nodeId, maxVelocity) { + var node = this.body.nodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; - this.options.width = width; - this.options.height = height; + // store the state so we can revert + this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; - emitEvent = true; + if (node.options.fixed.x === false) { + var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position } else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. - - if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - emitEvent = true; - } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; - } + forces[nodeId].x = 0; + velocities[nodeId].x = 0; } - if (emitEvent === true) { - this.body.emitter.emit('resize', { width: this.frame.canvas.width / this.pixelRatio, height: this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio }); + if (node.options.fixed.y === false) { + var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; } + + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; } }, { - key: '_XconvertDOMtoCanvas', + key: 'calculateForces', /** - * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} x - * @returns {number} - * @private + * calculate the forces for one physics iteration. */ - value: function _XconvertDOMtoCanvas(x) { - return (x - this.body.view.translation.x) / this.body.view.scale; + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); } }, { - key: '_XconvertCanvasToDOM', + key: '_freezeNodes', /** - * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the X coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} x - * @returns {number} + * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization + * because only the supportnodes for the smoothCurves have to settle. + * * @private */ - value: function _XconvertCanvasToDOM(x) { - return x * this.body.view.scale + this.body.view.translation.x; + value: function _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x && nodes[id].y) { + this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; + nodes[id].options.fixed.x = true; + nodes[id].options.fixed.y = true; + } + } + } } }, { - key: '_YconvertDOMtoCanvas', + key: '_restoreFrozenNodes', /** - * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} y - * @returns {number} + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * * @private */ - value: function _YconvertDOMtoCanvas(y) { - return (y - this.body.view.translation.y) / this.body.view.scale; + value: function _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (this.freezeCache[id] !== undefined) { + nodes[id].options.fixed.x = this.freezeCache[id].x; + nodes[id].options.fixed.y = this.freezeCache[id].y; + } + } + } + this.freezeCache = {}; } }, { - key: '_YconvertCanvasToDOM', + key: 'stabilize', /** - * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} y - * @returns {number} + * Find a stable position for all nodes * @private */ - value: function _YconvertCanvasToDOM(y) { - return y * this.body.view.scale + this.body.view.translation.y; + value: function stabilize() { + // stop the render loop + this.stopSimulation(); + + // set stabilze to false + this.stabilized = false; + + // block redraw requests + this.body.emitter.emit('_blockRedrawRequests'); + this.body.emitter.emit('startStabilizing'); + this.startedStabilization = true; + + // start the stabilization + if (this.options.stabilization.onlyDynamicEdges === true) { + this._freezeNodes(); + } + this.stabilizationIterations = 0; + + setTimeout(this._stabilizationBatch.bind(this), 0); } }, { - key: 'canvasToDOM', + key: '_stabilizationBatch', + value: function _stabilizationBatch() { + var count = 0; + while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.options.stabilization.iterations) { + this.physicsTick(); + this.stabilizationIterations++; + count++; + } + + if (this.stabilized === false && this.stabilizationIterations < this.options.stabilization.iterations) { + this.body.emitter.emit('stabilizationProgress', { iterations: this.stabilizationIterations, total: this.options.stabilization.iterations }); + setTimeout(this._stabilizationBatch.bind(this), 0); + } else { + this._finalizeStabilization(); + } + } + }, { + key: '_finalizeStabilization', + value: function _finalizeStabilization() { + this.body.emitter.emit('_allowRedrawRequests'); + if (this.options.stabilization.fit === true) { + this.body.emitter.emit('fit'); + } + + if (this.options.stabilization.onlyDynamicEdges === true) { + this._restoreFrozenNodes(); + } - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - value: function canvasToDOM(pos) { - return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; - } - }, { - key: 'DOMtoCanvas', + this.body.emitter.emit('stabilizationIterationsDone'); + this.body.emitter.emit('_requestRedraw'); - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - value: function DOMtoCanvas(pos) { - return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + this.ready = true; } }]); - return Canvas; + return PhysicsEngine; })(); - exports['default'] = Canvas; + exports['default'] = PhysicsEngine; module.exports = exports['default']; /***/ }, @@ -22105,394 +21945,644 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } - var util = __webpack_require__(1); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var View = (function () { - function View(body, canvas) { - var _this = this; + var _componentsNodesCluster = __webpack_require__(82); - _classCallCheck(this, View); + var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); - this.body = body; - this.canvas = canvas; + var util = __webpack_require__(1); - this.animationSpeed = 1 / this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - this.touchTime = 0; + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); - this.viewFunction = undefined; + this.body = body; + this.clusteredNodes = {}; - this.body.emitter.on("fit", this.fit.bind(this)); - this.body.emitter.on("animationFinished", function () { - _this.body.emitter.emit("_stopRendering"); - }); - this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); + this.options = {}; + this.defaultOptions = {}; + util.extend(this.options, this.defaultOptions); } - _createClass(View, [{ + _createClass(ClusterEngine, [{ key: "setOptions", - value: function setOptions() { + value: function setOptions(options) { + if (options !== undefined) {} + } + }, { + key: "clusterByHubsize", + + /** + * + * @param hubsize + * @param options + */ + value: function clusterByHubsize(hubsize, options) { + if (hubsize === undefined) { + hubsize = this._getHubSize(); + } else if (tyepof(hubsize) === "object") { + options = this._checkOptions(hubsize); + hubsize = this._getHubSize(); + } + + var nodesToCluster = []; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length >= hubsize) { + nodesToCluster.push(node.id); + } + } + + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, false); + } + this.body.emitter.emit("_dataChanged"); + } + }, { + key: "cluster", + + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param refreshData + */ + value: function cluster() { var options = arguments[0] === undefined ? {} : arguments[0]; + var refreshData = arguments[1] === undefined ? true : arguments[1]; - this.options = options; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } + + // check if the options object is fine, append if needed + options = this._checkOptions(options); + + var childNodesObj = {}; + var childEdgesObj = {}; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) === true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + } + }, { + key: "clusterOutliers", + + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + value: function clusterOutliers(options) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + + options = this._checkOptions(options); + var clusters = []; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var childNodesObj = {}; + var childEdgesObj = {}; + var nodeId = this.body.nodeIndices[i]; + if (this.body.nodes[nodeId].edges.length === 1) { + var edge = this.body.nodes[nodeId].edges[0]; + var childNodeId = this._getConnectedId(edge, nodeId); + if (childNodeId != nodeId) { + if (options.joinCondition === undefined) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) === true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + clonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(clonedOptions) === true) { + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); + } + } + } + + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, false); + } + + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + } + }, { + key: "clusterByConnection", + + /** + * suck all connected nodes of a node into the node. + * @param nodeId + * @param options + * @param refreshData + */ + value: function clusterByConnection(nodeId, options) { + var refreshData = arguments[2] === undefined ? true : arguments[2]; + + // kill conditions + if (nodeId === undefined) { + throw new Error("No nodeId supplied to clusterByConnection!"); + } + if (this.body.nodes[nodeId] === undefined) { + throw new Error("The nodeId given to clusterByConnection does not exist!"); + } + + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x; + } + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y; + } + if (options.clusterNodeProperties.fixed === undefined) { + options.clusterNodeProperties.fixed = {}; + options.clusterNodeProperties.fixed.x = node.options.fixed.x; + options.clusterNodeProperties.fixed.y = node.options.fixed.y; + } + + var childNodesObj = {}; + var childEdgesObj = {}; + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; + + // collect the nodes that will be in the cluster + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + var childNodeId = this._getConnectedId(edge, parentNodeId); + + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } else { + childEdgesObj[edge.id] = edge; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + } + }, { + key: "_cloneOptions", + + /** + * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + value: function _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type === "node") { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + clonedOptions.x = this.body.nodes[objId].x; + clonedOptions.y = this.body.nodes[objId].y; + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } else { + util.deepExtend(clonedOptions, this.body.edges[objId].options, true); + } + return clonedOptions; } }, { - key: "_getRange", + key: "_createClusterEdges", /** - * Find the center position of the network - * @private - */ - value: function _getRange() { - var specificNodes = arguments[0] === undefined ? [] : arguments[0]; + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { + var edge = undefined, + childNodeId = undefined, + childNode = undefined; - var minY = 1000000000, - maxY = -1000000000, - minX = 1000000000, - maxX = -1000000000, - node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.body.nodes[specificNodes[i]]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; + + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; + + var otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; + + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, "edge"); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive + clonedOptions.id = "clusterEdge:" + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)); } } } - - if (minX === 1000000000 && maxX === -1000000000 && minY === 1000000000 && maxY === -1000000000) { - minY = 0, maxY = 0, minX = 0, maxX = 0; - } - return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; } }, { - key: "_findCenter", + key: "_checkOptions", /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} - * @private - */ - value: function _findCenter(range) { - return { x: 0.5 * (range.maxX + range.minX), - y: 0.5 * (range.maxY + range.minY) }; + * This function checks the options that can be supplied to the different cluster functions + * for certain fields and inserts defaults if needed + * @param options + * @returns {*} + * @private + */ + value: function _checkOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + + if (options.clusterEdgeProperties === undefined) { + options.clusterEdgeProperties = {}; + } + if (options.clusterNodeProperties === undefined) { + options.clusterNodeProperties = {}; + } + + return options; } }, { - key: "fit", + key: "_cluster", /** - * This function zooms out to fit all data on screen based on amount of nodes - * @param {Object} Options - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - */ - value: function fit() { - var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; - var initialZoom = arguments[1] === undefined ? false : arguments[1]; + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} refreshData | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var refreshData = arguments[3] === undefined ? true : arguments[3]; - var range; - var zoomLevel; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length === 0) { + return; + } - if (initialZoom === true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.predefinedPosition === true) { - positionDefined += 1; - } - } + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + } + var clusterId = options.clusterNodeProperties.id; + + // construct the clusterNodeProperties + var clusterNodeProperties = options.clusterNodeProperties; + if (options.processProperties !== undefined) { + // get the childNode options + var childNodesOptions = []; + for (var nodeId in childNodesObj) { + var clonedOptions = this._cloneOptions(nodeId); + childNodesOptions.push(clonedOptions); } - if (positionDefined > 0.5 * this.body.nodeIndices.length) { - this.fit(options, false); - return; + + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, "edge"); + childEdgesOptions.push(clonedOptions); } - range = this._getRange(options.nodes); + clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); + if (!clusterNodeProperties) { + throw new Error("The processClusterProperties function does not return properties!"); + } + } + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = "cluster"; + } - var numberOfNodes = this.body.nodeIndices.length; - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + // give the clusterNode a postion if it does not have one. + var pos = undefined; + if (clusterNodeProperties.x === undefined) { + pos = this._getClusterPosition(childNodesObj); + clusterNodeProperties.x = pos.x; + } + if (clusterNodeProperties.y === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + } - // correct for larger canvasses. - var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); - zoomLevel *= factor; - } else { - this.body.emitter.emit("_redraw", true); - range = this._getRange(options.nodes); - var xDistance = Math.abs(range.maxX - range.minX) * 1.1; - var yDistance = Math.abs(range.maxY - range.minY) * 1.1; + // force the ID to remain the same + clusterNodeProperties.id = clusterId; - var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties, _componentsNodesCluster2["default"]); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; - zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + // finally put the cluster node into global + this.body.nodes[clusterNodeProperties.id] = clusterNode; + + // create the new edges that will connect to the cluster + var newEdges = []; + this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + + // disable the childEdges + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + edge.togglePhysics(false); + edge.options.hidden = true; + } + } } - if (zoomLevel > 1) { - zoomLevel = 1; - } else if (zoomLevel === 0) { - zoomLevel = 1; + // disable the childNodes + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + this.body.nodes[nodeId].togglePhysics(false); + this.body.nodes[nodeId].options.hidden = true; + } } - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options.animation }; - this.moveTo(animationOptions); + // push new edges to global + for (var i = 0; i < newEdges.length; i++) { + this.body.edges[newEdges[i].id] = newEdges[i]; + this.body.edges[newEdges[i].id].connect(); + } + + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; + + // wrap up + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } } }, { - key: "focus", - - // animation + key: "isCluster", /** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [options] - */ - value: function focus(nodeId) { - var options = arguments[1] === undefined ? {} : arguments[1]; - + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + value: function isCluster(nodeId) { if (this.body.nodes[nodeId] !== undefined) { - var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; - options.position = nodePosition; - options.lockedOnNode = nodeId; - - this.moveTo(options); + return this.body.nodes[nodeId].isCluster === true; } else { - console.log("Node: " + nodeId + " cannot be found."); + console.log("Node does not exist."); + return false; } } }, { - key: "moveTo", + key: "_getClusterPosition", /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to - */ - value: function moveTo(options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) { - options.offset = { x: 0, y: 0 }; - } - if (options.offset.x === undefined) { - options.offset.x = 0; - } - if (options.offset.y === undefined) { - options.offset.y = 0; - } - if (options.scale === undefined) { - options.scale = this.body.view.scale; - } - if (options.position === undefined) { - options.position = this.body.view.translation; - } - if (options.animation === undefined) { - options.animation = { duration: 0 }; - } - if (options.animation === false) { - options.animation = { duration: 0 }; - } - if (options.animation === true) { - options.animation = {}; + * get the position of the cluster node based on what's inside + * @param {object} childNodesObj | object with node objects, id as keys + * @returns {{x: number, y: number}} + * @private + */ + value: function _getClusterPosition(childNodesObj) { + var childKeys = Object.keys(childNodesObj); + var minX = childNodesObj[childKeys[0]].x; + var maxX = childNodesObj[childKeys[0]].x; + var minY = childNodesObj[childKeys[0]].y; + var maxY = childNodesObj[childKeys[0]].y; + var node = undefined; + for (var i = 0; i < childKeys.lenght; i++) { + node = childNodesObj[childKeys[0]]; + minX = node.x < minX ? node.x : minX; + maxX = node.x > maxX ? node.x : maxX; + minY = node.y < minY ? node.y : minY; + maxY = node.y > maxY ? node.y : maxY; } - if (options.animation.duration === undefined) { - options.animation.duration = 1000; - } // default duration - if (options.animation.easingFunction === undefined) { - options.animation.easingFunction = "easeInOutQuad"; - } // default easing function - - this.animateView(options); + return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; } }, { - key: "animateView", + key: "openCluster", /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.time = Number // animation time in milliseconds - * | options.scale = Number // scale to animate to - * | options.position = {x:Number, y:Number} // position to animate to - * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, - * // easeInCubic, easeOutCubic, easeInOutCubic, - * // easeInQuart, easeOutQuart, easeInOutQuart, - * // easeInQuint, easeOutQuint, easeInOutQuint - */ - value: function animateView(options) { - if (options === undefined) { + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} refreshData | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); + } + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster."); return; } - this.animationEasingFunction = options.animation.easingFunction; - // release if something focussed on the node - this.releaseNode(); - if (options.locked === true) { - this.lockedOnNodeId = options.lockedOnNode; - this.lockedOnNodeOffset = options.offset; + var clusterNode = this.body.nodes[clusterNodeId]; + var containedNodes = clusterNode.containedNodes; + var containedEdges = clusterNode.containedEdges; + + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + var containedNode = this.body.nodes[nodeId]; + containedNode = containedNodes[nodeId]; + // inherit position + containedNode.x = clusterNode.x; + containedNode.y = clusterNode.y; + + // inherit speed + containedNode.vx = clusterNode.vx; + containedNode.vy = clusterNode.vy; + + containedNode.options.hidden = false; + containedNode.togglePhysics(true); + + delete this.clusteredNodes[nodeId]; + } } - // forcefully complete the old animation if it was still running - if (this.easingTime != 0) { - this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + var edge = this.body.edges[edgeId]; + edge.options.hidden = false; + edge.togglePhysics(true); + } } - this.sourceScale = this.body.view.scale; - this.sourceTranslation = this.body.view.translation; - this.targetScale = options.scale; + // remove all temporary edges + for (var i = 0; i < clusterNode.edges.length; i++) { + var edgeId = clusterNode.edges[i].id; + this.body.edges[edgeId].edgeType.cleanup(); + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } - // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw - // but at least then we'll have the target transition - this.body.view.scale = this.targetScale; - var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - options.position.x, - y: viewCenter.y - options.position.y - }; - this.targetTranslation = { - x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, - y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y - }; + // remove clusterNode + delete this.body.nodes[clusterNodeId]; - // if the time is set to 0, don't do an animation - if (options.animation.duration === 0) { - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } else { - this.body.view.scale = this.targetScale; - this.body.view.translation = this.targetTranslation; - this.body.emitter.emit("_requestRedraw"); - } - } else { - this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's - this.animationEasingFunction = options.animation.easingFunction; + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + } + }, { + key: "_connectEdge", - this.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); + /** + * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to + * is currently residing in cluster B + * @param edge + * @param nodeId + * @param from + * @private + */ + value: function _connectEdge(edge, nodeId, from) { + var clusterStack = this.findNode(nodeId); + if (from === true) { + edge.from = clusterStack[clusterStack.length - 1]; + edge.fromId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.fromArray = clusterStack; + } else { + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; } + edge.connect(); } }, { - key: "_lockedRedraw", + key: "findNode", /** - * used to animate smoothly by hijacking the redraw function. - * @private - */ - value: function _lockedRedraw() { - var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; - var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this.body.view.translation; - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y - }; + * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node + * @param nodeId + * @returns {Array} + * @private + */ + value: function findNode(nodeId) { + var stack = []; + var max = 100; + var counter = 0; - this.body.view.translation = targetTranslation; + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; + } + stack.push(this.body.nodes[nodeId]); + return stack; } }, { - key: "releaseNode", - value: function releaseNode() { - if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; + key: "_getConnectedId", + + /** + * Get the Id the node is connected to + * @param edge + * @param nodeId + * @returns {*} + * @private + */ + value: function _getConnectedId(edge, nodeId) { + if (edge.toId != nodeId) { + return edge.toId; + } else if (edge.fromId != nodeId) { + return edge.fromId; + } else { + return edge.fromId; } } }, { - key: "_transitionRedraw", + key: "_getHubSize", /** - * - * @param easingTime - * @private - */ - value: function _transitionRedraw() { - var finished = arguments[0] === undefined ? false : arguments[0]; + * We determine how many connections denote an important hub. + * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) + * + * @private + */ + value: function _getHubSize() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; - this.easingTime += this.animationSpeed; - this.easingTime = finished === true ? 1 : this.easingTime; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length > largestHub) { + largestHub = node.edges.length; + } + average += node.edges.length; + averageSquared += Math.pow(node.edges.length, 2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + var letiance = averageSquared - Math.pow(average, 2); + var standardDeviation = Math.sqrt(letiance); - this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; - this.body.view.translation = { - x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, - y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress - }; + var hubThreshold = Math.floor(average + 2 * standardDeviation); - // cleanup - if (this.easingTime >= 1) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.easingTime = 0; - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } - this.body.emitter.emit("animationFinished"); + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; } - } - }, { - key: "getScale", - value: function getScale() { - return this.body.view.scale; - } - }, { - key: "getPosition", - value: function getPosition() { - return { x: this.body.view.translation.x, y: this.body.view.translation.y }; + + return hubThreshold; } }]); - return View; + return ClusterEngine; })(); - exports["default"] = View; + exports["default"] = ClusterEngine; module.exports = exports["default"]; /***/ }, @@ -22507,4035 +22597,4206 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNavigationHandler = __webpack_require__(81); - - var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); - - var _componentsPopup = __webpack_require__(82); - - var _componentsPopup2 = _interopRequireDefault(_componentsPopup); + if (typeof window !== 'undefined') { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + } var util = __webpack_require__(1); - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); + var CanvasRenderer = (function () { + function CanvasRenderer(body, canvas) { + _classCallCheck(this, CanvasRenderer); this.body = body; this.canvas = canvas; - this.selectionHandler = selectionHandler; - this.navigationHandler = new _componentsNavigationHandler2['default'](body, canvas); - - // bind the events from hammer to functions in this object - this.body.eventListeners.onTap = this.onTap.bind(this); - this.body.eventListeners.onTouch = this.onTouch.bind(this); - this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); - this.body.eventListeners.onHold = this.onHold.bind(this); - this.body.eventListeners.onDragStart = this.onDragStart.bind(this); - this.body.eventListeners.onDrag = this.onDrag.bind(this); - this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); - this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); - this.body.eventListeners.onPinch = this.onPinch.bind(this); - this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); - this.body.eventListeners.onRelease = this.onRelease.bind(this); - this.body.eventListeners.onContext = this.onContext.bind(this); - - this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.hoverObj = { nodes: {}, edges: {} }; - this.popup = undefined; - this.popupObj = undefined; - this.popupTimer = undefined; - this.body.functions.getPointer = this.getPointer.bind(this); + this.redrawRequested = false; + this.renderTimer = undefined; + this.requiresTimeout = true; + this.renderingActive = false; + this.renderRequests = 0; + this.pixelRatio = undefined; + this.allowRedrawRequests = true; + this.dragging = false; this.options = {}; this.defaultOptions = { - dragNodes: true, - dragView: true, - hoverEnabled: false, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - }, - navigationButtons: false, - tooltipDelay: 300, - zoomView: true + hideEdgesOnDrag: false, + hideNodesOnDrag: false }; util.extend(this.options, this.defaultOptions); + this._determineBrowserMethod(); this.bindEventListeners(); } - _createClass(InteractionHandler, [{ + _createClass(CanvasRenderer, [{ key: 'bindEventListeners', value: function bindEventListeners() { var _this = this; + this.body.emitter.on('dragStart', function () { + _this.dragging = true; + }); + this.body.emitter.on('dragEnd', function () { + return _this.dragging = false; + }); + this.body.emitter.on('_redraw', function () { + if (_this.renderingActive === false) { + _this._redraw(); + } + }); + this.body.emitter.on('_blockRedrawRequests', function () { + _this.allowRedrawRequests = false; + }); + this.body.emitter.on('_allowRedrawRequests', function () { + _this.allowRedrawRequests = true; + }); + this.body.emitter.on('_requestRedraw', this._requestRedraw.bind(this)); + this.body.emitter.on('_startRendering', function () { + _this.renderRequests += 1; + _this.renderingActive = true; + _this._startRendering(); + }); + this.body.emitter.on('_stopRendering', function () { + _this.renderRequests -= 1; + _this.renderingActive = _this.renderRequests > 0; + _this.renderTimer = undefined; + }); this.body.emitter.on('destroy', function () { - clearTimeout(_this.popupTimer); - delete _this.body.functions.getPointer; + _this.renderRequests = 0; + _this.renderingActive = false; + if (_this.requiresTimeout === true) { + clearTimeout(_this.renderTimer); + } else { + cancelAnimationFrame(_this.renderTimer); + } + _this.body.emitter.off(); }); } }, { key: 'setOptions', value: function setOptions(options) { if (options !== undefined) { - // extend all but the values in fields - var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag', 'keyboard', 'multiselect', 'selectable', 'selectConnectedEdges']; - util.selectiveNotDeepExtend(fields, this.options, options); - - // merge the keyboard options in. - util.mergeOptions(this.options, options, 'keyboard'); - - if (options.tooltip) { - util.extend(this.options.tooltip, options.tooltip); - if (options.tooltip.color) { - this.options.tooltip.color = util.parseColor(options.tooltip.color); + var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag']; + util.selectiveDeepExtend(fields, this.options, options); + } + } + }, { + key: '_startRendering', + value: function _startRendering() { + if (this.renderingActive === true) { + if (this.renderTimer === undefined) { + if (this.requiresTimeout === true) { + this.renderTimer = window.setTimeout(this._renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function + } else { + this.renderTimer = window.requestAnimationFrame(this._renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function } } } - - this.navigationHandler.setOptions(this.options); } }, { - key: 'getPointer', + key: '_renderStep', + value: function _renderStep() { + if (this.renderingActive === true) { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; - /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ - value: function getPointer(touch) { - return { - x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) - }; - } - }, { - key: 'onTouch', + if (this.requiresTimeout === true) { + // this schedules a new simulation step + this._startRendering(); + } + + this._redraw(); - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - value: function onTouch(event) { - if (new Date().valueOf() - this.touchTime > 50) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.body.view.scale; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + if (this.requiresTimeout === false) { + // this schedules a new simulation step + this._startRendering(); + } } } }, { - key: 'onTap', + key: 'redraw', /** - * handle tap/click event: select/unselect a node - * @private + * Redraw the network with the current data + * chart will be resized too. */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); - var multiselect = this.selectionHandler.options.multiselect && event.changedPointers[0].ctrlKey; - - this.checkSelectionChanges(pointer, event, multiselect); - this.selectionHandler._generateClickEvent('click', event, pointer); + value: function redraw() { + this.body.emitter.emit('setSize'); + this._redraw(); } }, { - key: 'onDoubleTap', + key: '_requestRedraw', /** - * handle doubletap event + * Redraw the network with the current data + * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. * @private */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent('doubleClick', event, pointer); + value: function _requestRedraw() { + if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedrawRequests === true) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, false), 0); + } else { + window.requestAnimationFrame(this._redraw.bind(this, false)); + } + } } }, { - key: 'onHold', + key: '_redraw', + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; - /** - * handle long tap event: multi select nodes - * @private - */ - value: function onHold(event) { - var pointer = this.getPointer(event.center); - var multiselect = this.selectionHandler.options.multiselect; + this.body.emitter.emit('initRedraw'); - this.checkSelectionChanges(pointer, event, multiselect); + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext('2d'); - this.selectionHandler._generateClickEvent('click', event, pointer); - this.selectionHandler._generateClickEvent('hold', event, pointer); - } - }, { - key: 'onRelease', + // when the container div was hidden, this fixes it back up! + if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) { + this.canvas.setSize(); + } - /** - * handle the release of the screen - * - * @private - */ - value: function onRelease(event) { - if (new Date().valueOf() - this.touchTime > 10) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent('release', event, pointer); - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + if (this.pixelRatio === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } - } - }, { - key: 'onContext', - value: function onContext(event) { - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - this.selectionHandler._generateClickEvent('oncontext', event, pointer); - } - }, { - key: 'checkSelectionChanges', - /** - * - * @param pointer - * @param add - */ - value: function checkSelectionChanges(pointer, event) { - var add = arguments[2] === undefined ? false : arguments[2]; + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - var previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - var previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - var previousSelection = this.selectionHandler.getSelection(); - var selected = undefined; - if (add === true) { - selected = this.selectionHandler.selectAdditionalOnPoint(pointer); - } else { - selected = this.selectionHandler.selectOnPoint(pointer); + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); + + this.body.emitter.emit('beforeDrawing', ctx); + + // set scaling and translation + ctx.save(); + ctx.translate(this.body.view.translation.x, this.body.view.translation.y); + ctx.scale(this.body.view.scale, this.body.view.scale); + + if (hidden === false) { + if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { + this._drawEdges(ctx); + } } - var selectedEdges = this.selectionHandler._getSelectedEdgeCount(); - var selectedNodes = this.selectionHandler._getSelectedNodeCount(); - if (selectedNodes - previouslySelectedNodeCount > 0) { - // node was selected - this.selectionHandler._generateClickEvent('selectNode', event, pointer); - selected = true; - } else if (selectedNodes - previouslySelectedNodeCount < 0) { - // node was deselected - this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); - selected = true; + if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { + this._drawNodes(ctx, hidden); } - if (selectedEdges - previouslySelectedEdgeCount > 0) { - // node was selected - this.selectionHandler._generateClickEvent('selectEdge', event, pointer); - selected = true; - } else if (selectedEdges - previouslySelectedEdgeCount < 0) { - // node was deselected - this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); - selected = true; + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); } - if (selected === true) { - // select or unselect - this.selectionHandler._generateClickEvent('select', event, pointer); + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); + + this.body.emitter.emit('afterDrawing', ctx); + + // restore original scaling and translation + ctx.restore(); + + if (hidden === true) { + ctx.clearRect(0, 0, w, h); } } }, { - key: 'onDragStart', + key: '_drawNodes', /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] * @private */ - value: function onDragStart(event) { - //in case the touch event was triggered on an external div, do the initial touch now. - if (this.drag.pointer === undefined) { - this.onTouch(event); - } - - // note: drag.pointer is set in onTouch to get the initial touch location - var node = this.selectionHandler.getNodeAt(this.drag.pointer); - - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = undefined; + value: function _drawNodes(ctx) { + var alwaysShow = arguments[1] === undefined ? false : arguments[1]; - this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer); + var nodes = this.body.nodes; + var nodeIndices = this.body.nodeIndices; + var node = undefined; + var selected = []; + var margin = 20; + var topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin }); + var bottomRight = this.canvas.DOMtoCanvas({ + x: this.canvas.frame.canvas.clientWidth + margin, + y: this.canvas.frame.canvas.clientHeight + margin + }); + var viewableArea = { top: topLeft.y, left: topLeft.x, bottom: bottomRight.y, right: bottomRight.x }; - if (node !== undefined && this.options.dragNodes === true) { - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (node.isSelected() === false) { - this.selectionHandler.unselectAll(); - this.selectionHandler.selectObject(node); + // draw unselected nodes; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + // set selected nodes aside + if (node.isSelected()) { + selected.push(nodeIndices[i]); + } else { + if (alwaysShow === true) { + node.draw(ctx); + } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) { + node.draw(ctx); + } } + } - var selection = this.selectionHandler.selectionObj.nodes; - // create an array with the selected nodes and their original location and status - for (var nodeId in selection) { - if (selection.hasOwnProperty(nodeId)) { - var object = selection[nodeId]; - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.options.fixed.x, - yFixed: object.options.fixed.y - }; + // draw the selected nodes on top + for (var i = 0; i < selected.length; i++) { + node = nodes[selected[i]]; + node.draw(ctx); + } + } + }, { + key: '_drawEdges', - object.options.fixed.x = true; - object.options.fixed.y = true; + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawEdges(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge = undefined; - this.drag.selection.push(s); - } + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edge.draw(ctx); } } } }, { - key: 'onDrag', + key: '_drawControlNodes', /** - * handle drag event + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function onDrag(event) { - var _this2 = this; + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge = undefined; - if (this.drag.pinched === true) { - return; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + edge._drawControlNodes(ctx); } + } + }, { + key: '_determineBrowserMethod', - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit('unlockNode'); + /** + * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because + * some implementations (safari and IE9) did not support requestAnimationFrame + * @private + */ + value: function _determineBrowserMethod() { + if (typeof window !== 'undefined') { + var browserType = navigator.userAgent.toLowerCase(); + this.requiresTimeout = false; + if (browserType.indexOf('msie 9.0') != -1) { + // IE 9 + this.requiresTimeout = true; + } else if (browserType.indexOf('safari') != -1) { + // safari + if (browserType.indexOf('chrome') <= -1) { + this.requiresTimeout = true; + } + } + } else { + this.requiresTimeout = true; + } + } + }]); - var pointer = this.getPointer(event.center); + return CanvasRenderer; + })(); - this.selectionHandler._generateClickEvent('dragging', event, pointer); + exports['default'] = CanvasRenderer; + module.exports = exports['default']; - var selection = this.drag.selection; - if (selection && selection.length && this.options.dragNodes === true) { - (function () { - // calculate delta's and new location - var deltaX = pointer.x - _this2.drag.pointer.x; - var deltaY = pointer.y - _this2.drag.pointer.y; +/***/ }, +/* 59 */ +/***/ function(module, exports, __webpack_require__) { - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; - // only move the node if it was not fixed initially - if (selection.xFixed === false) { - node.x = _this2.canvas._XconvertDOMtoCanvas(_this2.canvas._XconvertCanvasToDOM(selection.x) + deltaX); - } - // only move the node if it was not fixed initially - if (selection.yFixed === false) { - node.y = _this2.canvas._YconvertDOMtoCanvas(_this2.canvas._YconvertCanvasToDOM(selection.y) + deltaY); - } - }); + 'use strict'; - // start the simulation of the physics - _this2.body.emitter.emit('startSimulation'); - })(); - } else { - // move the network - if (this.options.dragView === true) { - // if the drag was not started properly because the click started outside the network div, start it now. - if (this.drag.pointer === undefined) { - this._handleDragStart(event); - return; - } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; - this.body.emitter.emit('_redraw'); + 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; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(48); + + var util = __webpack_require__(1); + + /** + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private + */ + + var Canvas = (function () { + function Canvas(body) { + _classCallCheck(this, Canvas); + + this.body = body; + this.pixelRatio = 1; + this.resizeTimer = undefined; + this.resizeFunction = this._onResize.bind(this); + + this.options = {}; + this.defaultOptions = { + autoResize: true, + height: '100%', + width: '100%' + }; + util.extend(this.options, this.defaultOptions); + + this.bindEventListeners(); + } + + _createClass(Canvas, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this = this; + + // bind the events + this.body.emitter.once('resize', function (obj) { + if (obj.width !== 0) { + _this.body.view.translation.x = obj.width * 0.5; } - } + if (obj.height !== 0) { + _this.body.view.translation.y = obj.height * 0.5; + } + }); + this.body.emitter.on('setSize', this.setSize.bind(this)); + this.body.emitter.on('destroy', function () { + _this.hammerFrame.destroy(); + _this.hammer.destroy(); + _this._cleanUp(); + }); } }, { - key: 'onDragEnd', + key: 'setOptions', + value: function setOptions(options) { + var _this2 = this; - /** - * handle drag start event - * @private - */ - value: function onDragEnd(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.options.fixed.x = s.xFixed; - s.node.options.fixed.y = s.yFixed; - }); - this.body.emitter.emit('startSimulation'); - } else { - this.body.emitter.emit('_requestRedraw'); + if (options !== undefined) { + var fields = ['width', 'height', 'autoResize']; + util.selectiveDeepExtend(fields, this.options, options); + } + + if (this.options.autoResize === true) { + // automatically adapt to a changing size of the browser. + this._cleanUp(); + this.resizeTimer = setInterval(function () { + _this2.setSize();_this2.body.emitter.emit('_requestRedraw'); + }, 1000); + this.resizeFunction = this._onResize.bind(this); + util.addEventListener(window, 'resize', this.resizeFunction); } - this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); } }, { - key: 'onPinch', + key: '_cleanUp', + value: function _cleanUp() { + // automatically adapt to a changing size of the browser. + if (this.resizeTimer !== undefined) { + clearInterval(this.resizeTimer); + } + util.removeEventListener(window, 'resize', this.resizeFunction); + this.resizeFunction = undefined; + } + }, { + key: '_onResize', + value: function _onResize() { + this.setSize(); + this.body.emitter.emit('_redraw'); + } + }, { + key: '_prepareValue', + value: function _prepareValue(value) { + if (typeof value === 'number') { + return value + 'px'; + } else if (typeof value === 'string') { + if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) { + return value; + } else if (value.indexOf('%') === -1) { + return value + 'px'; + } + } + throw new Error('Could not use the value supplie for width or height:' + value); + } + }, { + key: '_create', /** - * Handle pinch event - * @param event - * @private + * Create the HTML */ - value: function onPinch(event) { - var pointer = this.getPointer(event.center); + value: function _create() { + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } - this.drag.pinched = true; - if (this.pinch['scale'] === undefined) { - this.pinch.scale = 1; + this.frame = document.createElement('div'); + this.frame.className = 'vis-network'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window + + ////////////////////////////////////////////////////////////////// + + this.frame.canvas = document.createElement('canvas'); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } else { + var ctx = this.frame.canvas.getContext('2d'); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + + this.frame.canvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.scale; - this.zoom(scale, pointer); + // add the frame to the container element + this.body.container.appendChild(this.frame); + + this.body.view.scale = 1; + this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + + this._bindHammer(); } }, { - key: 'zoom', + key: '_bindHammer', /** - * Zoom the network in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer Position on screen - * @return {Number} appliedScale scale is limited within the boundaries + * This function binds hammer, it can be repeated over and over due to the uniqueness check. * @private */ - value: function zoom(scale, pointer) { - if (this.options.zoomView === true) { - var scaleOld = this.body.view.scale; - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } + value: function _bindHammer() { + var _this3 = this; - var preScaleDragPointer = undefined; - if (this.drag !== undefined) { - if (this.drag.dragging === true) { - preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - } - } - // + this.canvas.frame.canvas.clientHeight / 2 - var translation = this.body.view.translation; + if (this.hammer !== undefined) { + this.hammer.destroy(); + } + this.drag = {}; + this.pinch = {}; - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + // init hammer + this.hammer = new Hammer(this.frame.canvas); + this.hammer.get('pinch').set({ enable: true }); + // enable to get better response, todo: test on mobile. + //this.hammer.get('pan').set({threshold:2}); - this.body.view.scale = scale; - this.body.view.translation = { x: tx, y: ty }; + hammerUtil.onTouch(this.hammer, function (event) { + _this3.body.eventListeners.onTouch(event); + }); + this.hammer.on('tap', function (event) { + _this3.body.eventListeners.onTap(event); + }); + this.hammer.on('doubletap', function (event) { + _this3.body.eventListeners.onDoubleTap(event); + }); + this.hammer.on('press', function (event) { + _this3.body.eventListeners.onHold(event); + }); + this.hammer.on('panstart', function (event) { + _this3.body.eventListeners.onDragStart(event); + }); + this.hammer.on('panmove', function (event) { + _this3.body.eventListeners.onDrag(event); + }); + this.hammer.on('panend', function (event) { + _this3.body.eventListeners.onDragEnd(event); + }); + this.hammer.on('pinch', function (event) { + _this3.body.eventListeners.onPinch(event); + }); - if (preScaleDragPointer != undefined) { - var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } + // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? + this.frame.canvas.addEventListener('mousewheel', function (event) { + _this3.body.eventListeners.onMouseWheel(event); + }); + this.frame.canvas.addEventListener('DOMMouseScroll', function (event) { + _this3.body.eventListeners.onMouseWheel(event); + }); - this.body.emitter.emit('_requestRedraw'); + this.frame.canvas.addEventListener('mousemove', function (event) { + _this3.body.eventListeners.onMouseMove(event); + }); + this.frame.canvas.addEventListener('contextmenu', function (event) { + _this3.body.eventListeners.onContext(event); + }); - if (scaleOld < scale) { - this.body.emitter.emit('zoom', { direction: '+' }); - } else { - this.body.emitter.emit('zoom', { direction: '-' }); - } - } + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, function (event) { + _this3.body.eventListeners.onRelease(event); + }); } }, { - key: 'onMouseWheel', + key: 'setSize', /** - * Event handler for mouse wheel event, used to zoom the timeline - * See http://adomas.org/javascript-mouse-wheel/ - * https://github.com/EightMedia/hammer.js/issues/256 - * @param {MouseEvent} event - * @private + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ - value: function onMouseWheel(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } + value: function setSize() { + var width = arguments[0] === undefined ? this.options.width : arguments[0]; + var height = arguments[1] === undefined ? this.options.height : arguments[1]; - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta !== 0) { + width = this._prepareValue(width); + height = this._prepareValue(height); - // calculate the new scale - var scale = this.body.view.scale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= 1 + zoom; + var emitEvent = false; + var oldWidth = this.frame.canvas.width; + var oldHeight = this.frame.canvas.height; - // calculate the pointer location - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { + this.frame.style.width = width; + this.frame.style.height = height; - // apply the new scale - this.zoom(scale, pointer); - } + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - // Prevent default actions caused by mouse wheel. - event.preventDefault(); - } - }, { - key: 'onMouseMove', + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ - value: function onMouseMove(event) { - var _this3 = this; + this.options.width = width; + this.options.height = height; - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - var popupVisible = false; + emitEvent = true; + } else { + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); + if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + emitEvent = true; } - - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.show(); + if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + emitEvent = true; } } - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. - if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) { - this.canvas.frame.focus(); + if (emitEvent === true) { + this.body.emitter.emit('resize', { width: this.frame.canvas.width / this.pixelRatio, height: this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio }); } + } + }, { + key: '_XconvertDOMtoCanvas', - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - if (this.popupTimer !== undefined) { - clearInterval(this.popupTimer); // stop any running calculationTimer - this.popupTimer = undefined; - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(function () { - return _this3._checkShowPopup(pointer); - }, this.options.tooltipDelay); - } - } + /** + * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} x + * @returns {number} + * @private + */ + value: function _XconvertDOMtoCanvas(x) { + return (x - this.body.view.translation.x) / this.body.view.scale; + } + }, { + key: '_XconvertCanvasToDOM', - /** - * Adding hover highlights - */ - if (this.options.hoverEnabled === true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } + /** + * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the X coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} x + * @returns {number} + * @private + */ + value: function _XconvertCanvasToDOM(x) { + return x * this.body.view.scale + this.body.view.translation.x; + } + }, { + key: '_YconvertDOMtoCanvas', + + /** + * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} y + * @returns {number} + * @private + */ + value: function _YconvertDOMtoCanvas(y) { + return (y - this.body.view.translation.y) / this.body.view.scale; + } + }, { + key: '_YconvertCanvasToDOM', - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj === undefined) { - obj = this.selectionHandler.getEdgeAt(pointer); - } - if (obj != undefined) { - this.selectionHandler.hoverObject(obj); - } + /** + * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} y + * @returns {number} + * @private + */ + value: function _YconvertCanvasToDOM(y) { + return y * this.body.view.scale + this.body.view.translation.y; + } + }, { + key: 'canvasToDOM', - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj === undefined) { - this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.body.emitter.emit('_requestRedraw'); - } + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + value: function canvasToDOM(pos) { + return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; } }, { - key: '_checkShowPopup', + key: 'DOMtoCanvas', /** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. * - * @param {{x:Number, y:Number}} pointer - * @private + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor */ - value: function _checkShowPopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + value: function DOMtoCanvas(pos) { + return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + } + }]); - var previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = 'node'; + return Canvas; + })(); - // check if a node is under the cursor. - if (this.popupObj === undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodeIndices = this.body.nodeIndices; - var nodes = this.body.nodes; - var node = undefined; - var overlappingNodes = []; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - if (node.isOverlappingWith(pointerObj) === true) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(nodeIndices[i]); - } - } - } + exports['default'] = Canvas; + module.exports = exports['default']; - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others - this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } +/***/ }, +/* 60 */ +/***/ function(module, exports, __webpack_require__) { - if (this.popupObj === undefined && nodeUnderCursor === false) { - // search the edges for overlap - var edgeIndices = this.body.edgeIndices; - var edges = this.body.edges; - var edge = undefined; - var overlappingEdges = []; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.isOverlappingWith(pointerObj) === true) { - if (edge.connected === true && edge.getTitle() !== undefined) { - overlappingEdges.push(edgeIndices[i]); - } - } - } + "use strict"; - if (overlappingEdges.length > 0) { - this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = 'edge'; - } - } + Object.defineProperty(exports, "__esModule", { + value: true + }); - if (this.popupObj !== undefined) { - // show popup message window - if (this.popupObj.id !== previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new _componentsPopup2['default'](this.canvas.frame); - } + 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; }; })(); - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); - this.body.emitter.emit('showPopup', this.popupObj.id); - } - } else { - if (this.popup !== undefined) { - this.popup.hide(); - this.body.emitter.emit('hidePopup'); - } - } + var util = __webpack_require__(1); + + var View = (function () { + function View(body, canvas) { + var _this = this; + + _classCallCheck(this, View); + + this.body = body; + this.canvas = canvas; + + this.animationSpeed = 1 / this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + this.touchTime = 0; + + this.viewFunction = undefined; + + this.body.emitter.on("fit", this.fit.bind(this)); + this.body.emitter.on("animationFinished", function () { + _this.body.emitter.emit("_stopRendering"); + }); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); + } + + _createClass(View, [{ + key: "setOptions", + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + + this.options = options; } }, { - key: '_checkHidePopup', + key: "_getRange", /** - * Check if the popup must be hidden, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer + * Find the center position of the network * @private */ - value: function _checkHidePopup(pointer) { - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - - var stillOnObj = false; - if (this.popup.popupTargetType === 'node') { - if (this.body.nodes[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + value: function _getRange() { + var specificNodes = arguments[0] === undefined ? [] : arguments[0]; - // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. - // we initially only check stillOnObj because this is much faster. - if (stillOnObj === true) { - var overNode = this.selectionHandler.getNodeAt(pointer); - stillOnObj = overNode.id === this.popup.popupTargetId; + var minY = 1000000000, + maxY = -1000000000, + minX = 1000000000, + maxX = -1000000000, + node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive } } else { - if (this.selectionHandler.getNodeAt(pointer) === undefined) { - if (this.body.edges[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; + } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive } } } - if (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); - this.body.emitter.emit('hidePopup'); + if (minX === 1000000000 && maxX === -1000000000 && minY === 1000000000 && maxY === -1000000000) { + minY = 0, maxY = 0, minX = 0, maxX = 0; } + return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; + } + }, { + key: "_findCenter", + + /** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; } - }]); + }, { + key: "fit", - return InteractionHandler; - })(); + /** + * This function zooms out to fit all data on screen based on amount of nodes + * @param {Object} Options + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + */ + value: function fit() { + var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; + var initialZoom = arguments[1] === undefined ? false : arguments[1]; - exports['default'] = InteractionHandler; - module.exports = exports['default']; + var range; + var zoomLevel; -/***/ }, -/* 59 */ -/***/ function(module, exports, __webpack_require__) { + if (initialZoom === true) { + // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. + var positionDefined = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.predefinedPosition === true) { + positionDefined += 1; + } + } + } + if (positionDefined > 0.5 * this.body.nodeIndices.length) { + this.fit(options, false); + return; + } - "use strict"; + range = this._getRange(options.nodes); - Object.defineProperty(exports, "__esModule", { - value: true - }); + var numberOfNodes = this.body.nodeIndices.length; + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - 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; }; })(); + // correct for larger canvasses. + var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); + zoomLevel *= factor; + } else { + this.body.emitter.emit("_redraw", true); + range = this._getRange(options.nodes); + var xDistance = Math.abs(range.maxX - range.minX) * 1.1; + var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; - var Node = __webpack_require__(71); - var util = __webpack_require__(1); + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; + if (zoomLevel > 1) { + zoomLevel = 1; + } else if (zoomLevel === 0) { + zoomLevel = 1; + } - _classCallCheck(this, SelectionHandler); + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options.animation }; + this.moveTo(animationOptions); + } + }, { + key: "focus", - this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; + // animation - this.options = {}; - this.defaultOptions = { - multiselect: false, - selectable: true, - selectConnectedEdges: true - }; - util.extend(this.options, this.defaultOptions); + /** + * Center a node in view. + * + * @param {Number} nodeId + * @param {Number} [options] + */ + value: function focus(nodeId) { + var options = arguments[1] === undefined ? {} : arguments[1]; - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); - } + if (this.body.nodes[nodeId] !== undefined) { + var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; + options.position = nodePosition; + options.lockedOnNode = nodeId; - _createClass(SelectionHandler, [{ - key: "setOptions", - value: function setOptions(options) { - if (options !== undefined) { - var fields = ["multiselect", "selectable", "selectConnectedEdges"]; - util.selectiveDeepExtend(fields, this.options, options); + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); } } }, { - key: "selectOnPoint", + key: "moveTo", /** - * handles the selection part of the tap; * - * @param {Object} pointer - * @private + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.scale = Number // scale to move to + * | options.position = {x:Number, y:Number} // position to move to + * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.selectable === true) { - this.unselectAll(); - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - if (obj !== undefined) { - selected = this.selectObject(obj); - } - this.body.emitter.emit("_requestRedraw"); + value: function moveTo(options) { + if (options === undefined) { + options = {}; + return; } - return selected; - } - }, { - key: "selectAdditionalOnPoint", - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.selectable === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); - } else { - this.selectObject(obj); - } - - this.body.emitter.emit("_requestRedraw"); - } + if (options.offset === undefined) { + options.offset = { x: 0, y: 0 }; } - return selectionChanged; - } - }, { - key: "_generateClickEvent", - value: function _generateClickEvent(eventType, event, pointer, oldSelection) { - var properties = this.getSelection(); - properties["pointer"] = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - properties["event"] = event; - - if (oldSelection !== undefined) { - properties["previousSelection"] = oldSelection; + if (options.offset.x === undefined) { + options.offset.x = 0; } - this.body.emitter.emit(eventType, properties); + if (options.offset.y === undefined) { + options.offset.y = 0; + } + if (options.scale === undefined) { + options.scale = this.body.view.scale; + } + if (options.position === undefined) { + options.position = this.body.view.translation; + } + if (options.animation === undefined) { + options.animation = { duration: 0 }; + } + if (options.animation === false) { + options.animation = { duration: 0 }; + } + if (options.animation === true) { + options.animation = {}; + } + if (options.animation.duration === undefined) { + options.animation.duration = 1000; + } // default duration + if (options.animation.easingFunction === undefined) { + options.animation.easingFunction = "easeInOutQuad"; + } // default easing function + + this.animateView(options); } }, { - key: "selectObject", - value: function selectObject(obj) { - var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; + key: "animateView", - if (obj !== undefined) { - if (obj instanceof Node) { - if (highlightEdges === true) { - this._selectConnectedEdges(obj); - } - } - obj.select(); - this._addToSelection(obj); - return true; + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.time = Number // animation time in milliseconds + * | options.scale = Number // scale to animate to + * | options.position = {x:Number, y:Number} // position to animate to + * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, + * // easeInCubic, easeOutCubic, easeInOutCubic, + * // easeInQuart, easeOutQuart, easeInOutQuart, + * // easeInQuint, easeOutQuint, easeInOutQuint + */ + value: function animateView(options) { + if (options === undefined) { + return; } - return false; - } - }, { - key: "deselectObject", - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); + this.animationEasingFunction = options.animation.easingFunction; + // release if something focussed on the node + this.releaseNode(); + if (options.locked === true) { + this.lockedOnNodeId = options.lockedOnNode; + this.lockedOnNodeOffset = options.offset; } - } - }, { - key: "_getAllNodesOverlappingWith", - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); + // forcefully complete the old animation if it was still running + if (this.easingTime != 0) { + this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + } + + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; + + // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw + // but at least then we'll have the target transition + this.body.view.scale = this.targetScale; + var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - options.position.x, + y: viewCenter.y - options.position.y + }; + this.targetTranslation = { + x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, + y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y + }; + + // if the time is set to 0, don't do an animation + if (options.animation.duration === 0) { + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } else { + this.body.view.scale = this.targetScale; + this.body.view.translation = this.targetTranslation; + this.body.emitter.emit("_requestRedraw"); } + } else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; + + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); } - return overlappingNodes; } }, { - key: "_pointerToPositionObject", + key: "_lockedRedraw", /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} + * used to animate smoothly by hijacking the redraw function. * @private */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x - 1, - top: canvasPos.y + 1, - right: canvasPos.x + 1, - bottom: canvasPos.y - 1 + value: function _lockedRedraw() { + var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; + var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - nodePosition.x, + y: viewCenter.y - nodePosition.y + }; + var sourceTranslation = this.body.view.translation; + var targetTranslation = { + x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, + y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y }; + + this.body.view.translation = targetTranslation; } }, { - key: "getNodeAt", - - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | undefined} node - * @private - */ - value: function getNodeAt(pointer) { - var returnNode = arguments[1] === undefined ? true : arguments[1]; - - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - if (returnNode === true) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return overlappingNodes[overlappingNodes.length - 1]; - } - } else { - return undefined; + key: "releaseNode", + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; } } }, { - key: "_getEdgesOverlappingWith", + key: "_transitionRedraw", /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * + * @param easingTime * @private */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + value: function _transitionRedraw() { + var finished = arguments[0] === undefined ? false : arguments[0]; + + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1 : this.easingTime; + + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + + this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; + this.body.view.translation = { + x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, + y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress + }; + + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); } + this.body.emitter.emit("animationFinished"); } } }, { - key: "_getAllEdgesOverlappingWith", - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllEdgesOverlappingWith(object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; + key: "getScale", + value: function getScale() { + return this.body.view.scale; } }, { - key: "getEdgeAt", + key: "getPosition", + value: function getPosition() { + return { x: this.body.view.translation.x, y: this.body.view.translation.y }; + } + }]); - /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {undefined} - * @private - */ - value: function getEdgeAt(pointer) { - var returnEdge = arguments[1] === undefined ? true : arguments[1]; + return View; + })(); - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + exports["default"] = View; + module.exports = exports["default"]; - if (overlappingEdges.length > 0) { - if (returnEdge === true) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return overlappingEdges[overlappingEdges.length - 1]; - } - } else { - return undefined; - } +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + 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; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _componentsNavigationHandler = __webpack_require__(83); + + var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); + + var _componentsPopup = __webpack_require__(84); + + var _componentsPopup2 = _interopRequireDefault(_componentsPopup); + + var util = __webpack_require__(1); + + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); + + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + this.navigationHandler = new _componentsNavigationHandler2['default'](body, canvas); + + // bind the events from hammer to functions in this object + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); + this.body.eventListeners.onContext = this.onContext.bind(this); + + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.hoverObj = { nodes: {}, edges: {} }; + this.popup = undefined; + this.popupObj = undefined; + this.popupTimer = undefined; + + this.body.functions.getPointer = this.getPointer.bind(this); + + this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + hoverEnabled: false, + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true + }, + navigationButtons: false, + tooltipDelay: 300, + zoomView: true + }; + util.extend(this.options, this.defaultOptions); + + this.bindEventListeners(); + } + + _createClass(InteractionHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this = this; + + this.body.emitter.on('destroy', function () { + clearTimeout(_this.popupTimer); + delete _this.body.functions.getPointer; + }); } }, { - key: "_addToSelection", + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + // extend all but the values in fields + var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag', 'keyboard', 'multiselect', 'selectable', 'selectConnectedEdges']; + util.selectiveNotDeepExtend(fields, this.options, options); - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + // merge the keyboard options in. + util.mergeOptions(this.options, options, 'keyboard'); + + if (options.tooltip) { + util.extend(this.options.tooltip, options.tooltip); + if (options.tooltip.color) { + this.options.tooltip.color = util.parseColor(options.tooltip.color); + } + } } + + this.navigationHandler.setOptions(this.options); } }, { - key: "_addToHover", + key: 'getPointer', /** - * Add object to the selection array. - * - * @param obj + * Get the pointer location from a touch location + * @param {{x: Number, y: Number}} touch + * @return {{x: Number, y: Number}} pointer * @private */ - value: function _addToHover(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } else { - this.hoverObj.edges[obj.id] = obj; - } + value: function getPointer(touch) { + return { + x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) + }; } }, { - key: "_removeFromSelection", + key: 'onTouch', /** - * Remove a single option from selection. - * - * @param {Object} obj + * On start of a touch gesture, store the pointer + * @param event * @private */ - value: function _removeFromSelection(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 50) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); } } }, { - key: "unselectAll", + key: 'onTap', /** - * Unselect all. The selectionObj is useful for this. - * + * handle tap/click event: select/unselect a node * @private */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } + value: function onTap(event) { + var pointer = this.getPointer(event.center); + var multiselect = this.selectionHandler.options.multiselect && event.changedPointers[0].ctrlKey; - this.selectionObj = { nodes: {}, edges: {} }; + this.checkSelectionChanges(pointer, event, multiselect); + this.selectionHandler._generateClickEvent('click', event, pointer); } }, { - key: "_getSelectedNodeCount", + key: 'onDoubleTap', /** - * return the number of selected nodes - * - * @returns {number} + * handle doubletap event * @private */ - value: function _getSelectedNodeCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent('doubleClick', event, pointer); } }, { - key: "_getSelectedNode", + key: 'onHold', /** - * return the selected node - * - * @returns {number} + * handle long tap event: multi select nodes * @private */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return undefined; + value: function onHold(event) { + var pointer = this.getPointer(event.center); + var multiselect = this.selectionHandler.options.multiselect; + + this.checkSelectionChanges(pointer, event, multiselect); + + this.selectionHandler._generateClickEvent('click', event, pointer); + this.selectionHandler._generateClickEvent('hold', event, pointer); } }, { - key: "_getSelectedEdge", + key: 'onRelease', /** - * return the selected edge + * handle the release of the screen * - * @returns {number} * @private */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } + value: function onRelease(event) { + if (new Date().valueOf() - this.touchTime > 10) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent('release', event, pointer); + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); } - return undefined; } }, { - key: "_getSelectedEdgeCount", + key: 'onContext', + value: function onContext(event) { + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + this.selectionHandler._generateClickEvent('oncontext', event, pointer); + } + }, { + key: 'checkSelectionChanges', /** - * return the number of selected edges * - * @returns {number} - * @private + * @param pointer + * @param add */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } + value: function checkSelectionChanges(pointer, event) { + var add = arguments[2] === undefined ? false : arguments[2]; + + var previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var previousSelection = this.selectionHandler.getSelection(); + var selected = undefined; + if (add === true) { + selected = this.selectionHandler.selectAdditionalOnPoint(pointer); + } else { + selected = this.selectionHandler.selectOnPoint(pointer); + } + var selectedEdges = this.selectionHandler._getSelectedEdgeCount(); + var selectedNodes = this.selectionHandler._getSelectedNodeCount(); + + if (selectedNodes - previouslySelectedNodeCount > 0) { + // node was selected + this.selectionHandler._generateClickEvent('selectNode', event, pointer); + selected = true; + } else if (selectedNodes - previouslySelectedNodeCount < 0) { + // node was deselected + this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); + selected = true; + } + + if (selectedEdges - previouslySelectedEdgeCount > 0) { + // node was selected + this.selectionHandler._generateClickEvent('selectEdge', event, pointer); + selected = true; + } else if (selectedEdges - previouslySelectedEdgeCount < 0) { + // node was deselected + this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); + selected = true; + } + + if (selected === true) { + // select or unselect + this.selectionHandler._generateClickEvent('select', event, pointer); } - return count; } }, { - key: "_getSelectedObjectCount", + key: 'onDragStart', /** - * return the number of selected objects. + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. * - * @returns {number} * @private */ - value: function _getSelectedObjectCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } + value: function onDragStart(event) { + //in case the touch event was triggered on an external div, do the initial touch now. + if (this.drag.pointer === undefined) { + this.onTouch(event); } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + + // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); + + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = undefined; + + this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer); + + if (node !== undefined && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); + } + + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (var nodeId in selection) { + if (selection.hasOwnProperty(nodeId)) { + var object = selection[nodeId]; + var s = { + id: object.id, + node: object, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y + }; + + object.options.fixed.x = true; + object.options.fixed.y = true; + + this.drag.selection.push(s); + } } } - return count; } }, { - key: "_selectionIsEmpty", + key: 'onDrag', /** - * Check if anything is selected - * - * @returns {boolean} + * handle drag event * @private */ - value: function _selectionIsEmpty() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } + value: function onDrag(event) { + var _this2 = this; + + if (this.drag.pinched === true) { + return; } - return true; - } - }, { - key: "_clusterInSelection", - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - value: function _clusterInSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit('unlockNode'); + + var pointer = this.getPointer(event.center); + + this.selectionHandler._generateClickEvent('dragging', event, pointer); + + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + (function () { + // calculate delta's and new location + var deltaX = pointer.x - _this2.drag.pointer.x; + var deltaY = pointer.y - _this2.drag.pointer.y; + + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + // only move the node if it was not fixed initially + if (selection.xFixed === false) { + node.x = _this2.canvas._XconvertDOMtoCanvas(_this2.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + // only move the node if it was not fixed initially + if (selection.yFixed === false) { + node.y = _this2.canvas._YconvertDOMtoCanvas(_this2.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); + + // start the simulation of the physics + _this2.body.emitter.emit('startSimulation'); + })(); + } else { + // move the network + if (this.options.dragView === true) { + // if the drag was not started properly because the click started outside the network div, start it now. + if (this.drag.pointer === undefined) { + this._handleDragStart(event); + return; } + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit('_redraw'); } } - return false; } }, { - key: "_selectConnectedEdges", + key: 'onDragEnd', /** - * select the edges connected to the node that is being selected - * - * @param {Node} node + * handle drag start event * @private */ - value: function _selectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); + value: function onDragEnd(event) { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection && selection.length) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; + }); + this.body.emitter.emit('startSimulation'); + } else { + this.body.emitter.emit('_requestRedraw'); } + this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); } }, { - key: "_hoverConnectedEdges", + key: 'onPinch', /** - * select the edges connected to the node that is being selected - * - * @param {Node} node + * Handle pinch event + * @param event * @private */ - value: function _hoverConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); + value: function onPinch(event) { + var pointer = this.getPointer(event.center); + + this.drag.pinched = true; + if (this.pinch['scale'] === undefined) { + this.pinch.scale = 1; } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); } }, { - key: "_unselectConnectedEdges", + key: 'zoom', /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node + * Zoom the network in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer Position on screen + * @return {Number} appliedScale scale is limited within the boundaries * @private */ - value: function _unselectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); + value: function zoom(scale, pointer) { + if (this.options.zoomView === true) { + var scaleOld = this.body.view.scale; + if (scale < 0.00001) { + scale = 0.00001; + } + if (scale > 10) { + scale = 10; + } + + var preScaleDragPointer = undefined; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + } + } + // + this.canvas.frame.canvas.clientHeight / 2 + var translation = this.body.view.translation; + + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + + this.body.view.scale = scale; + this.body.view.translation = { x: tx, y: ty }; + + if (preScaleDragPointer != undefined) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; + } + + this.body.emitter.emit('_requestRedraw'); + + if (scaleOld < scale) { + this.body.emitter.emit('zoom', { direction: '+' }); + } else { + this.body.emitter.emit('zoom', { direction: '-' }); + } } } }, { - key: "blurObject", + key: 'onMouseWheel', /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event * @private */ - value: function blurObject(object) { - if (object.hover === true) { - object.hover = false; - this.body.emitter.emit("blurNode", { node: object.id }); + value: function onMouseWheel(event) { + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; } - } - }, { - key: "hoverObject", - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function hoverObject(object) { - if (object.hover === false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.body.emitter.emit("hoverNode", { node: object.id }); + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta !== 0) { + + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); - } - } - }, { - key: "getSelection", + scale *= 1 + zoom; - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - value: function getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; + // calculate the pointer location + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + + // apply the new scale + this.zoom(scale, pointer); + } + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); } }, { - key: "getSelectedNodes", + key: 'onMouseMove', /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private */ - value: function getSelectedNodes() { - var idArray = []; - if (this.options.selectable === true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } + value: function onMouseMove(event) { + var _this3 = this; + + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + var popupVisible = false; + + // check if the previously selected node is still selected + if (this.popup !== undefined) { + if (this.popup.hidden === false) { + this._checkHidePopup(pointer); + } + + // if the popup was not hidden above + if (this.popup.hidden === false) { + popupVisible = true; + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.show(); } } - return idArray; - } - }, { - key: "getSelectedEdges", - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - value: function getSelectedEdges() { - var idArray = []; - if (this.options.selectable === true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); + // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. + if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) { + this.canvas.frame.focus(); + } + + // start a timeout that will check if the mouse is positioned above an element + if (popupVisible === false) { + if (this.popupTimer !== undefined) { + clearInterval(this.popupTimer); // stop any running calculationTimer + this.popupTimer = undefined; + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(function () { + return _this3._checkShowPopup(pointer); + }, this.options.tooltipDelay); + } + } + + /** + * Adding hover highlights + */ + if (this.options.hoverEnabled === true) { + // removing all hover highlights + for (var edgeId in this.hoverObj.edges) { + if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + this.hoverObj.edges[edgeId].hover = false; + delete this.hoverObj.edges[edgeId]; + } + } + + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj === undefined) { + obj = this.selectionHandler.getEdgeAt(pointer); + } + if (obj != undefined) { + this.selectionHandler.hoverObject(obj); + } + + // removing all node hover highlights except for the selected one. + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj === undefined) { + this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } } } + this.body.emitter.emit('_requestRedraw'); } - return idArray; } }, { - key: "selectNodes", + key: '_checkShowPopup', /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer + * @private */ - value: function selectNodes(selection) { - var highlightEdges = arguments[1] === undefined ? true : arguments[1]; - - var i = undefined, - id = undefined; - - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; + value: function _checkShowPopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; - // first unselect any selected node - this.unselectAll(); + var previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = 'node'; - for (i = 0; i < selection.length; i++) { - id = selection[i]; + // check if a node is under the cursor. + if (this.popupObj === undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodeIndices = this.body.nodeIndices; + var nodes = this.body.nodes; + var node = undefined; + var overlappingNodes = []; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + if (node.isOverlappingWith(pointerObj) === true) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(nodeIndices[i]); + } + } + } - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others + this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; } - this.selectObject(node, highlightEdges); } - this.body.emitter.emit("_requestRedraw"); - } - }, { - key: "selectEdges", - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - value: function selectEdges(selection) { - var i = undefined, - id = undefined; + if (this.popupObj === undefined && nodeUnderCursor === false) { + // search the edges for overlap + var edgeIndices = this.body.edgeIndices; + var edges = this.body.edges; + var edge = undefined; + var overlappingEdges = []; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.isOverlappingWith(pointerObj) === true) { + if (edge.connected === true && edge.getTitle() !== undefined) { + overlappingEdges.push(edgeIndices[i]); + } + } + } - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; + if (overlappingEdges.length > 0) { + this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = 'edge'; + } + } - // first unselect any selected objects - this.unselectAll(); + if (this.popupObj !== undefined) { + // show popup message window + if (this.popupObj.id !== previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new _componentsPopup2['default'](this.canvas.frame); + } - for (i = 0; i < selection.length; i++) { - id = selection[i]; + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - var edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + this.body.emitter.emit('showPopup', this.popupObj.id); + } + } else { + if (this.popup !== undefined) { + this.popup.hide(); + this.body.emitter.emit('hidePopup'); } - this.selectObject(edge); } - this.body.emitter.emit("_requestRedraw"); } }, { - key: "updateSelection", + key: '_checkHidePopup', /** - * Validate the selection: remove ids of nodes which no longer exist + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer * @private */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; + value: function _checkHidePopup(pointer) { + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + + var stillOnObj = false; + if (this.popup.popupTargetType === 'node') { + if (this.body.nodes[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + + // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. + // we initially only check stillOnObj because this is much faster. + if (stillOnObj === true) { + var overNode = this.selectionHandler.getNodeAt(pointer); + stillOnObj = overNode.id === this.popup.popupTargetId; } } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; + } else { + if (this.selectionHandler.getNodeAt(pointer) === undefined) { + if (this.body.edges[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } } + + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + this.body.emitter.emit('hidePopup'); + } } }]); - return SelectionHandler; + return InteractionHandler; })(); - exports["default"] = SelectionHandler; - module.exports = exports["default"]; + exports['default'] = InteractionHandler; + module.exports = exports['default']; /***/ }, -/* 60 */ +/* 62 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + Object.defineProperty(exports, "__esModule", { value: true }); - 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; }; })(); + 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; }; })(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + var Node = __webpack_require__(73); var util = __webpack_require__(1); - var LayoutEngine = (function () { - function LayoutEngine(body) { - _classCallCheck(this, LayoutEngine); + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + var _this = this; + + _classCallCheck(this, SelectionHandler); this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; - this.initialRandomSeed = Math.round(Math.random() * 1000000); - this.randomSeed = this.initialRandomSeed; this.options = {}; - this.optionsBackup = {}; - this.defaultOptions = { - randomSeed: undefined, - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: 'UD', // UD, DU, LR, RL - sortMethod: 'hubsize' // hubsize, directed - } + multiselect: false, + selectable: true, + selectConnectedEdges: true }; util.extend(this.options, this.defaultOptions); - this.hierarchicalLevels = {}; - - this.bindEventListeners(); + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); } - _createClass(LayoutEngine, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this = this; - - this.body.emitter.on('_dataChanged', function () { - _this.setupHierarchicalLayout(); - }); - this.body.emitter.on('_resetHierarchicalLayout', function () { - _this.setupHierarchicalLayout(); - }); + _createClass(SelectionHandler, [{ + key: "setOptions", + value: function setOptions(options) { + if (options !== undefined) { + var fields = ["multiselect", "selectable", "selectConnectedEdges"]; + util.selectiveDeepExtend(fields, this.options, options); + } } }, { - key: 'setOptions', - value: function setOptions(options, allOptions) { - if (options !== undefined) { - var prevHierarchicalState = this.options.hierarchical.enabled; + key: "selectOnPoint", - util.mergeOptions(this.options, options, 'hierarchical'); - if (options.randomSeed !== undefined) { - this.randomSeed = options.randomSeed; + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.selectable === true) { + this.unselectAll(); + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + if (obj !== undefined) { + selected = this.selectObject(obj); } + this.body.emitter.emit("_requestRedraw"); + } + return selected; + } + }, { + key: "selectAdditionalOnPoint", + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.selectable === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - if (this.options.hierarchical.enabled === true) { - // make sure the level seperation is the right way up - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; - } + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; - } + this.selectObject(obj); } - this.body.emitter.emit('_resetHierarchicalLayout'); - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. - return this.adaptAllOptions(allOptions); - } else { - if (prevHierarchicalState === true) { - // refresh the overridden options for nodes and edges. - this.body.emitter.emit('refresh'); - return util.deepExtend(allOptions, this.optionsBackup); - } + this.body.emitter.emit("_requestRedraw"); } } - return allOptions; + return selectionChanged; } }, { - key: 'adaptAllOptions', - value: function adaptAllOptions(allOptions) { - if (this.options.hierarchical.enabled === true) { - // set the physics - if (allOptions.physics === undefined || allOptions.physics === true) { - allOptions.physics = { solver: 'hierarchicalRepulsion' }; - this.optionsBackup.physics = { solver: 'barnesHut' }; - } else if (typeof options.physics === 'object') { - this.optionsBackup.physics = { solver: 'barnesHut' }; - if (options.physics.solver !== undefined) { - this.optionsBackup.physics = { solver: options.physics.solver }; + key: "_generateClickEvent", + value: function _generateClickEvent(eventType, event, pointer, oldSelection) { + var properties = this.getSelection(); + properties["pointer"] = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + properties["event"] = event; + + if (oldSelection !== undefined) { + properties["previousSelection"] = oldSelection; + } + this.body.emitter.emit(eventType, properties); + } + }, { + key: "selectObject", + value: function selectObject(obj) { + var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; + + if (obj !== undefined) { + if (obj instanceof Node) { + if (highlightEdges === true) { + this._selectConnectedEdges(obj); } - allOptions.physics['solver'] = 'hierarchicalRepulsion'; - } else if (options.physics !== false) { - this.optionsBackup.physics = { solver: 'barnesHut' }; - allOptions.physics['solver'] = 'hierarchicalRepulsion'; } + obj.select(); + this._addToSelection(obj); + return true; + } + return false; + } + }, { + key: "deselectObject", + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); + } + } + }, { + key: "_getAllNodesOverlappingWith", - // get the type of static smooth curve in case it is required - var type = 'horizontal'; - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { - type = 'vertical'; + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + var nodes = this.body.nodes; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); } + } + return overlappingNodes; + } + }, { + key: "_pointerToPositionObject", - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. - if (allOptions.edges === undefined) { - this.optionsBackup.edges = { smooth: true, dynamic: true }; - allOptions.edges = { smooth: false }; - } else if (allOptions.edges.smooth === undefined) { - this.optionsBackup.edges = { smooth: true, dynamic: true }; - allOptions.edges.smooth = false; + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x - 1, + top: canvasPos.y + 1, + right: canvasPos.x + 1, + bottom: canvasPos.y - 1 + }; + } + }, { + key: "getNodeAt", + + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node + * @private + */ + value: function getNodeAt(pointer) { + var returnNode = arguments[1] === undefined ? true : arguments[1]; + + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + if (returnNode === true) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; } else { - if (typeof allOptions.edges.smooth === 'boolean') { - this.optionsBackup.edges = { smooth: allOptions.edges.smooth, dynamic: true }; - allOptions.edges.smooth = { enabled: allOptions.edges.smooth, dynamic: false, type: type }; - } else { - this.optionsBackup.edges = { smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic: true }; - allOptions.edges.smooth = { enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic: false, type: type }; - } + return overlappingNodes[overlappingNodes.length - 1]; } - - // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. - this.body.emitter.emit('_forceDisableDynamicCurves', type); + } else { + return undefined; } - return allOptions; - } - }, { - key: 'seededRandom', - value: function seededRandom() { - var x = Math.sin(this.randomSeed++) * 10000; - return x - Math.floor(x); } }, { - key: 'positionInitially', - value: function positionInitially(nodesArray) { - if (this.options.hierarchical.enabled !== true) { - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { - var radius = 10 * 0.1 * nodesArray.length + 10; - var angle = 2 * Math.PI * this.seededRandom(); + key: "_getEdgesOverlappingWith", - if (node.options.fixed.x === false) { - node.x = radius * Math.cos(angle); - } - if (node.options.fixed.x === false) { - node.y = radius * Math.sin(angle); - } - } + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); } } } }, { - key: 'getSeed', - value: function getSeed() { - return this.initialRandomSeed; + key: "_getAllEdgesOverlappingWith", + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; } }, { - key: 'setupHierarchicalLayout', + key: "getEdgeAt", /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * + * @param pointer + * @returns {undefined} * @private */ - value: function setupHierarchicalLayout() { - if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var node = undefined, - nodeId = undefined; - var definedLevel = false; - var undefinedLevel = false; - this.hierarchicalLevels = {}; - this.nodeSpacing = 100; + value: function getEdgeAt(pointer) { + var returnEdge = arguments[1] === undefined ? true : arguments[1]; - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.options.level !== undefined) { - definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; - } else { - undefinedLevel = true; - } - } - } + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel === true && definedLevel === true) { - throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); - return; + if (overlappingEdges.length > 0) { + if (returnEdge === true) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; } else { - // setup the system to use hierarchical method. - //this._changeConstants(); - - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel === true) { - if (this.options.hierarchical.sortMethod === 'hubsize') { - this._determineLevelsByHubsize(); - } else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') { - this._determineLevelsDirected(); - } - } - - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - - // place the nodes on the canvas. - this._placeNodesByHierarchy(distribution); + return overlappingEdges[overlappingEdges.length - 1]; } + } else { + return undefined; } } }, { - key: '_placeNodesByHierarchy', + key: "_addToSelection", /** - * This function places the nodes on the canvas based on the hierarchial distribution. + * Add object to the selection array. * - * @param {Object} distribution | obtained by the function this._getDistribution() + * @param obj * @private */ - value: function _placeNodesByHierarchy(distribution) { - var nodeId = undefined, - node = undefined; - this.positionedNodes = {}; - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - - node = distribution[level].nodes[nodeId]; - - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - if (node.x === undefined) { - node.x = distribution[level].distance; - } - distribution[level].distance = node.x + this.nodeSpacing; - } else { - if (node.y === undefined) { - node.y = distribution[level].distance; - } - distribution[level].distance = node.y + this.nodeSpacing; - } - - this.positionedNodes[nodeId] = true; - this._placeBranchNodes(node.edges, node.id, distribution, level); - } - } - } + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; } } }, { - key: '_getDistribution', + key: "_addToHover", /** - * This function get the distribution of levels based on hubsize + * Add object to the selection array. * - * @returns {Object} + * @param obj * @private */ - value: function _getDistribution() { - var distribution = {}; - var nodeId = undefined, - node = undefined; - - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - var level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - node.y = this.options.hierarchical.levelSeparation * level; - node.options.fixed.y = true; - } else { - node.x = this.options.hierarchical.levelSeparation * level; - node.options.fixed.x = true; - } - if (distribution[level] === undefined) { - distribution[level] = { amount: 0, nodes: {}, distance: 0 }; - } - distribution[level].amount += 1; - distribution[level].nodes[nodeId] = node; - } + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; } - return distribution; } }, { - key: '_getHubSize', + key: "_removeFromSelection", /** - * Get the hubsize from all remaining unlevelled nodes. + * Remove a single option from selection. * - * @returns {number} + * @param {Object} obj * @private */ - value: function _getHubSize() { - var hubSize = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { - hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; - } - } + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; } - return hubSize; } }, { - key: '_determineLevelsByHubsize', + key: "unselectAll", /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * Unselect all. The selectionObj is useful for this. * - * @param hubsize * @private */ - value: function _determineLevelsByHubsize() { - var nodeId = undefined, - node = undefined; - var hubSize = 1; + value: function unselectAll() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } - while (hubSize > 0) { - // determine hubs - hubSize = this._getHubSize(); - if (hubSize === 0) break; + this.selectionObj = { nodes: {}, edges: {} }; + } + }, { + key: "_getSelectedNodeCount", - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length === hubSize) { - this._setLevel(0, node); - } - } + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } } + return count; } }, { - key: '_setLevel', + key: "_getSelectedNode", /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * return the selected node * - * @param level - * @param edges - * @param parentId + * @returns {number} * @private */ - value: function _setLevel(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) return; - - var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId === node.id) { - childNode = node.edges[i].from; - } else { - childNode = node.edges[i].to; + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; } - this._setLevel(level + 1, childNode); } + return undefined; } }, { - key: '_determineLevelsDirected', + key: "_getSelectedEdge", /** - * this function allocates nodes in levels based on the direction of the edges + * return the selected edge * - * @param hubsize + * @returns {number} * @private */ - value: function _determineLevelsDirected() { - var nodeId = undefined, - node = undefined; - var minLevel = 10000; - - // set first node to source - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - this._setLevelDirected(minLevel, node); - } - } - - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; } } + return undefined; + } + }, { + key: "_getSelectedEdgeCount", - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.hierarchicalLevels[nodeId] -= minLevel; + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } } + return count; } }, { - key: '_setLevelDirected', + key: "_getSelectedObjectCount", /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction + * return the number of selected objects. * - * @param level - * @param edges - * @param parentId + * @returns {number} * @private */ - value: function _setLevelDirected(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) return; - - var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId === node.id) { - childNode = node.edges[i].from; - this._setLevelDirected(level - 1, childNode); - } else { - childNode = node.edges[i].to; - this._setLevelDirected(level + 1, childNode); + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } } + return count; } }, { - key: '_placeBranchNodes', + key: "_selectionIsEmpty", /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. + * Check if anything is selected * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel + * @returns {boolean} * @private */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = undefined; - var parentNode = undefined; - if (edges[i].toId === parentId) { - childNode = edges[i].from; - parentNode = edges[i].to; - } else { - childNode = edges[i].to; - parentNode = edges[i].from; + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; } - var childNodeLevel = this.hierarchicalLevels[childNode.id]; - - if (this.positionedNodes[childNode.id] === undefined) { - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - if (childNodeLevel > parentLevel) { - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - if (childNode.x === undefined) { - childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); - } - distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; - this.positionedNodes[childNode.id] = true; - } else { - if (childNode.y === undefined) { - childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); - } - distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; - } - this.positionedNodes[childNode.id] = true; - - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); - } - } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; } } + return true; } - }]); - - return LayoutEngine; - })(); - - exports['default'] = LayoutEngine; - module.exports = exports['default']; - -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(1); - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(45); - - /** - * clears the toolbar div element of children - * - * @private - */ - - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - var _this = this; - - _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.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.inMode = false; - this.selectedControlNode = undefined; - - 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.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(ManipulationSystem, [{ - key: '_restore', + }, { + key: "_clusterInSelection", /** - * 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(); + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } } } + return false; } }, { - key: 'setOptions', + key: "_selectConnectedEdges", /** - * Set the Options - * @param options + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - value: function setOptions(options, allOptions) { - if (allOptions !== undefined) { - if (allOptions.locale !== undefined) { - this.options.locale = allOptions.locale; - }; - if (allOptions.locales !== undefined) { - this.options.locales = allOptions.locales; - }; - } - - 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(); + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); } } }, { - key: 'toggleEditMode', + key: "_hoverConnectedEdges", /** - * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * select the edges connected to the node that is being selected * + * @param {Node} node * @private */ - value: function toggleEditMode() { - if (this.editMode === true) { - this.disableEditMode(); - } else { - this.enableEditMode(); + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); } } }, { - key: 'enableEditMode', - value: function enableEditMode() { - this.editMode = true; + key: "_unselectConnectedEdges", - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'block'; - this.closeDiv.style.display = 'block'; - this.editModeDiv.style.display = 'none'; - this.showManipulatorToolbar(); + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); } } }, { - key: 'disableEditMode', - value: function disableEditMode() { - this.editMode = false; + key: "blurObject", - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'none'; - this.closeDiv.style.display = 'none'; - this.editModeDiv.style.display = 'block'; - this._createEditButton(); + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function blurObject(object) { + if (object.hover === true) { + object.hover = false; + this.body.emitter.emit("blurNode", { node: object.id }); } } }, { - key: 'showManipulatorToolbar', + key: "hoverObject", /** - * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection * + * @param {Node || Edge} object * @private */ - value: function showManipulatorToolbar() { - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - - // reset global letiables - this.manipulationDOM = {}; - - // 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'; - - 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; - - 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); + value: function hoverObject(object) { + if (object.hover === false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + } + }, { + key: "getSelection", - 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); - } + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + } + }, { + key: "getSelectedNodes", - // 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); + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.selectable === true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); } } - - // 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)); } - - // redraw to show any possible changes - this.body.emitter.emit('_redraw'); + return idArray; } }, { - key: 'addNodeMode', + key: "getSelectedEdges", /** - * Create the toolbar for adding Nodes * - * @private + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. */ - value: function addNodeMode() { - // 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(); - - 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)); + value: function getSelectedEdges() { + var idArray = []; + if (this.options.selectable === true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } } - - this._temporaryBindEvent('click', this._performAddNode.bind(this)); + return idArray; } }, { - key: 'editNodeMode', + key: "selectNodes", /** - * call the bound function to handle the editing of the node. The node has to be selected. - * - * @private + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] */ - value: function editNodeMode() { - var _this2 = this; + value: function selectNodes(selection) { + var highlightEdges = arguments[1] === undefined ? true : arguments[1]; - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + var i = undefined, + id = undefined; - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - this.inMode = 'editNode'; - if (typeof this.options.editNode === 'function') { - var node = this.selectionHandler._getSelectedNode(); - if (node.isCluster !== true) { - var data = util.deepExtend({}, node.options, true); - data.x = node.x; - data.y = node.y; + // first unselect any selected node + this.unselectAll(); - if (this.options.editNode.length === 2) { - this.options.editNode(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'delete') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this2.body.data.nodes.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']); + for (i = 0; i < selection.length; i++) { + id = selection[i]; + + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); } - } else { - throw new Error('No function has been configured to handle the editing of nodes.'); + this.selectObject(node, highlightEdges); } + this.body.emitter.emit("_requestRedraw"); } }, { - key: 'addEdgeMode', + key: "selectEdges", /** - * create the toolbar to connect nodes - * - * @private + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - value: function addEdgeMode() { - // 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(); + value: function selectEdges(selection) { + var i = undefined, + id = undefined; - 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']); + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + // first unselect any selected objects + this.unselectAll(); - // 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)); + for (i = 0; i < selection.length; i++) { + id = selection[i]; - this._temporaryBindUI('onDragStart', function () {}); - this._temporaryBindUI('onHold', function () {}); + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this.selectObject(edge); + } + this.body.emitter.emit("_requestRedraw"); } }, { - key: 'editEdgeMode', + key: "updateSelection", /** - * create the toolbar to edit edges - * + * Validate the selection: remove ids of nodes which no longer exist * @private */ - value: function editEdgeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } } + } + }]); - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + return SelectionHandler; + })(); - 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']); + exports["default"] = SelectionHandler; + module.exports = exports["default"]; - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - var edge = this.body.edges[this.edgeBeingEditedId]; + 'use strict'; - // create control nodes - var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); - var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.temporaryIds.nodes.push(controlNodeFrom.id); - this.temporaryIds.nodes.push(controlNodeTo.id); + 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; }; })(); - this.body.nodes[controlNodeFrom.id] = controlNodeFrom; - this.body.nodeIndices.push(controlNodeFrom.id); - this.body.nodes[controlNodeTo.id] = controlNodeTo; - this.body.nodeIndices.push(controlNodeTo.id); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI - this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position - this._temporaryBindUI('onTap', function () {}); // disabled - this._temporaryBindUI('onHold', function () {}); // disabled - this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this)); // used to select control node - this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node - this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes - this._temporaryBindUI('onMouseMove', function () {}); // disabled + var util = __webpack_require__(1); - // create function to position control nodes correctly on movement - // automatically cleaned up because we use the temporary bind - this._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 LayoutEngine = (function () { + function LayoutEngine(body) { + _classCallCheck(this, LayoutEngine); - this.body.emitter.emit('_redraw'); - } - }, { - key: 'deleteSelected', + this.body = body; - /** - * delete everything in the selection - * - * @private - */ - value: function deleteSelected() { - var _this3 = this; + this.initialRandomSeed = Math.round(Math.random() * 1000000); + this.randomSeed = this.initialRandomSeed; + this.options = {}; + this.optionsBackup = {}; - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); + this.defaultOptions = { + randomSeed: undefined, + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: 'UD', // UD, DU, LR, RL + sortMethod: 'hubsize' // hubsize, directed } + }; + util.extend(this.options, this.defaultOptions); - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - - 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.hierarchicalLevels = {}; - if (typeof this.options.deleteNode === 'function') { - deleteFunction = this.options.deleteNode; - } - } else if (selectedEdges.length > 0) { - if (typeof this.options.deleteEdge === 'function') { - deleteFunction = this.options.deleteEdge; - } - } + this.bindEventListeners(); + } - if (typeof deleteFunction === 'function') { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length === 2) { - deleteFunction(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this3.inMode === 'delete') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this3.body.data.edges.remove(finalizedData.edges); - _this3.body.data.nodes.remove(finalizedData.nodes); - _this3.body.emitter.emit('startSimulation'); - } - }); - } else { - throw new Error('The function for delete does not support two arguments (data, callback)'); - } - } else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this.body.emitter.emit('startSimulation'); - } + _createClass(LayoutEngine, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this = this; + + this.body.emitter.on('_dataChanged', function () { + _this.setupHierarchicalLayout(); + }); + this.body.emitter.on('_resetHierarchicalLayout', function () { + _this.setupHierarchicalLayout(); + }); } }, { - key: '_setup', + key: 'setOptions', + value: function setOptions(options, allOptions) { + if (options !== undefined) { + var prevHierarchicalState = this.options.hierarchical.enabled; - //********************************************** PRIVATE ***************************************// + util.mergeOptions(this.options, options, 'hierarchical'); + if (options.randomSeed !== undefined) { + this.randomSeed = options.randomSeed; + } - /** - * draw or remove the DOM - * @private - */ - value: function _setup() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; + if (this.options.hierarchical.enabled === true) { + // make sure the level seperation is the right way up + if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { + if (this.options.hierarchical.levelSeparation > 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } else { + if (this.options.hierarchical.levelSeparation < 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } - this._createWrappers(); - if (this.editMode === false) { - this._createEditButton(); + this.body.emitter.emit('_resetHierarchicalLayout'); + // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + return this.adaptAllOptions(allOptions); } else { - this.showManipulatorToolbar(); + if (prevHierarchicalState === true) { + // refresh the overridden options for nodes and edges. + this.body.emitter.emit('refresh'); + return util.deepExtend(allOptions, this.optionsBackup); + } } - } else { - this._removeManipulationDOM(); - - // disable the gui - this.guiEnabled = false; } + return allOptions; } }, { - key: '_createWrappers', + key: 'adaptAllOptions', + value: function adaptAllOptions(allOptions) { + if (this.options.hierarchical.enabled === true) { + // set the physics + if (allOptions.physics === undefined || allOptions.physics === true) { + allOptions.physics = { solver: 'hierarchicalRepulsion' }; + this.optionsBackup.physics = { solver: 'barnesHut' }; + } else if (typeof options.physics === 'object') { + this.optionsBackup.physics = { solver: 'barnesHut' }; + if (options.physics.solver !== undefined) { + this.optionsBackup.physics = { solver: options.physics.solver }; + } + allOptions.physics['solver'] = 'hierarchicalRepulsion'; + } else if (options.physics !== false) { + this.optionsBackup.physics = { solver: 'barnesHut' }; + allOptions.physics['solver'] = 'hierarchicalRepulsion'; + } - /** - * 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'; + // get the type of static smooth curve in case it is required + var type = 'horizontal'; + if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { + type = 'vertical'; } - 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'; + // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + if (allOptions.edges === undefined) { + this.optionsBackup.edges = { smooth: true, dynamic: true }; + allOptions.edges = { smooth: false }; + } else if (allOptions.edges.smooth === undefined) { + this.optionsBackup.edges = { smooth: true, dynamic: true }; + allOptions.edges.smooth = false; } else { - this.editModeDiv.style.display = 'block'; + if (typeof allOptions.edges.smooth === 'boolean') { + this.optionsBackup.edges = { smooth: allOptions.edges.smooth, dynamic: true }; + allOptions.edges.smooth = { enabled: allOptions.edges.smooth, dynamic: false, type: type }; + } else { + this.optionsBackup.edges = { smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic: true }; + allOptions.edges.smooth = { enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic: false, type: type }; + } } - this.canvas.frame.appendChild(this.editModeDiv); + + // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. + this.body.emitter.emit('_forceDisableDynamicCurves', type); } + return allOptions; + } + }, { + key: 'seededRandom', + value: function seededRandom() { + var x = Math.sin(this.randomSeed++) * 10000; + return x - Math.floor(x); + } + }, { + key: 'positionInitially', + value: function positionInitially(nodesArray) { + if (this.options.hierarchical.enabled !== true) { + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { + var radius = 10 * 0.1 * nodesArray.length + 10; + var angle = 2 * Math.PI * this.seededRandom(); - // 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); + if (node.options.fixed.x === false) { + node.x = radius * Math.cos(angle); + } + if (node.options.fixed.x === false) { + node.y = radius * Math.sin(angle); + } + } + } } } }, { - key: '_getNewTargetNode', + key: 'getSeed', + value: function getSeed() { + return this.initialRandomSeed; + } + }, { + key: 'setupHierarchicalLayout', /** - * generate a new target node. Used for creating new edges and editing edges - * @param x - * @param y - * @returns {*} + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * * @private */ - value: function _getNewTargetNode(x, y) { - var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + value: function setupHierarchicalLayout() { + if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var node = undefined, + nodeId = undefined; + var definedLevel = false; + var undefinedLevel = false; + this.hierarchicalLevels = {}; + this.nodeSpacing = 100; - controlNodeStyle.id = 'targetNode' + util.randomUUID(); - controlNodeStyle.hidden = false; - controlNodeStyle.physics = false; - controlNodeStyle.x = x; - controlNodeStyle.y = y; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.options.level !== undefined) { + definedLevel = true; + this.hierarchicalLevels[nodeId] = node.options.level; + } else { + undefinedLevel = true; + } + } + } - return this.body.functions.createNode(controlNodeStyle); + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel === true && definedLevel === true) { + throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); + return; + } else { + // setup the system to use hierarchical method. + //this._changeConstants(); + + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel === true) { + if (this.options.hierarchical.sortMethod === 'hubsize') { + this._determineLevelsByHubsize(); + } else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') { + this._determineLevelsDirected(); + } + } + + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); + + // place the nodes on the canvas. + this._placeNodesByHierarchy(distribution); + } + } } }, { - key: '_createEditButton', + key: '_placeNodesByHierarchy', /** - * Create the edit button + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private */ - value: function _createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); - - // reset the manipulationDOM - this.manipulationDOM = {}; - - // empty the editModeDiv - util.recursiveDOMDelete(this.editModeDiv); + value: function _placeNodesByHierarchy(distribution) { + var nodeId = undefined, + node = undefined; + this.positionedNodes = {}; + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { - // 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); + node = distribution[level].nodes[nodeId]; - // bind a hammer listener to the button, calling the function toggleEditMode. - this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if (node.x === undefined) { + node.x = distribution[level].distance; + } + distribution[level].distance = node.x + this.nodeSpacing; + } else { + if (node.y === undefined) { + node.y = distribution[level].distance; + } + distribution[level].distance = node.y + this.nodeSpacing; + } + + this.positionedNodes[nodeId] = true; + this._placeBranchNodes(node.edges, node.id, distribution, level); + } + } + } + } } }, { - key: '_clean', + key: '_getDistribution', /** - * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * This function get the distribution of levels based on hubsize + * + * @returns {Object} * @private */ - value: function _clean() { - // not in mode - this.inMode = false; - - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); + value: function _getDistribution() { + var distribution = {}; + var nodeId = undefined, + node = undefined; - // removes all the bindings and overloads - this._cleanManipulatorHammers(); + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + var level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + node.y = this.options.hierarchical.levelSeparation * level; + node.options.fixed.y = true; + } else { + node.x = this.options.hierarchical.levelSeparation * level; + node.options.fixed.x = true; + } + if (distribution[level] === undefined) { + distribution[level] = { amount: 0, nodes: {}, distance: 0 }; + } + distribution[level].amount += 1; + distribution[level].nodes[nodeId] = node; + } } - - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); - - // restore overloaded UI functions - this._unbindTemporaryUIs(); - - // remove the temporaryEventFunctions - this._unbindTemporaryEvents(); - - // restore the physics if required - this.body.emitter.emit('restorePhysics'); + return distribution; } }, { - key: '_cleanManipulatorHammers', + key: '_getHubSize', /** - * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * Get the hubsize from all remaining unlevelled nodes. + * + * @returns {number} * @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(); + value: function _getHubSize() { + var hubSize = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.hierarchicalLevels[nodeId] === undefined) { + hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + } } - this.manipulationHammers = []; } + return hubSize; } }, { - key: '_removeManipulationDOM', + key: '_determineLevelsByHubsize', /** - * Remove all DOM elements created by this module. + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize * @private */ - value: function _removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); - - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + value: function _determineLevelsByHubsize() { + var nodeId = undefined, + node = undefined; + var hubSize = 1; - // remove the manipulation divs - this.canvas.frame.removeChild(this.manipulationDiv); - this.canvas.frame.removeChild(this.editModeDiv); - this.canvas.frame.removeChild(this.closeDiv); + while (hubSize > 0) { + // determine hubs + hubSize = this._getHubSize(); + if (hubSize === 0) break; - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length === hubSize) { + this._setLevel(0, node); + } + } + } + } } }, { - key: '_createSeperator', + key: '_setLevel', /** - * create a seperator line. the index is to differentiate in the manipulation dom - * @param index + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * + * @param level + * @param edges + * @param parentId * @private */ - value: function _createSeperator() { - var index = arguments[0] === undefined ? 1 : arguments[0]; - - this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); - } - }, { - key: '_createAddNodeButton', - - // ---------------------- DOM functions for buttons --------------------------// - - 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('editNodeMode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editNodeMode.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]; + value: function _setLevel(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) return; - 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)); + var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId === node.id) { + childNode = node.edges[i].from; + } else { + childNode = node.edges[i].to; + } + this._setLevel(level + 1, childNode); + } } }, { - key: '_temporaryBindEvent', - - // -------------------------- End of DOM functions for buttons ------------------------------// + key: '_determineLevelsDirected', /** - * this binds an event until cleanup by the clean functions. - * @param event - * @param newFunction + * this function allocates nodes in levels based on the direction of the edges + * + * @param hubsize * @private */ - value: function _temporaryBindEvent(event, newFunction) { - this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); - this.body.emitter.on(event, newFunction); - } - }, { - key: '_temporaryBindUI', + value: function _determineLevelsDirected() { + var nodeId = undefined, + node = undefined; + var minLevel = 10000; - /** - * 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))); + // set first node to source + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + this._setLevelDirected(minLevel, node); + } + } + + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.hierarchicalLevels[nodeId] -= minLevel; + } } } }, { - key: '_unbindTemporaryUIs', + key: '_setLevelDirected', /** - * Restore the overridden UI functions to their original state. + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * + * @param level + * @param edges + * @param parentId * @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]; + value: function _setLevelDirected(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) return; + + var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId === node.id) { + childNode = node.edges[i].from; + this._setLevelDirected(level - 1, childNode); + } else { + childNode = node.edges[i].to; + this._setLevelDirected(level + 1, childNode); } } - this.temporaryUIFunctions = {}; } }, { - key: '_unbindTemporaryEvents', + key: '_placeBranchNodes', /** - * Unbind the events created by _temporaryBindEvent + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. + * + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @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 = []; - } - }, { - key: '_bindHammerToDiv', + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = undefined; + var parentNode = undefined; + if (edges[i].toId === parentId) { + childNode = edges[i].from; + parentNode = edges[i].to; + } else { + childNode = edges[i].to; + parentNode = edges[i].from; + } + var childNodeLevel = this.hierarchicalLevels[childNode.id]; - /** - * 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); - } - }, { - key: '_cleanupTemporaryNodesAndEdges', + if (this.positionedNodes[childNode.id] === undefined) { + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + if (childNodeLevel > parentLevel) { + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if (childNode.x === undefined) { + childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); + } + distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; + this.positionedNodes[childNode.id] = true; + } else { + if (childNode.y === undefined) { + childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); + } + distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; + } + this.positionedNodes[childNode.id] = true; - /** - * 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); + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + } + } } } + } + }]); - // _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); - } - } + return LayoutEngine; + })(); - this.temporaryIds = { nodes: [], edges: [] }; - } - }, { - key: '_controlNodeTouch', + exports['default'] = LayoutEngine; + module.exports = exports['default']; - // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// +/***/ }, +/* 64 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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', + 'use strict'; - /** - * 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; + Object.defineProperty(exports, '__esModule', { + value: true + }); - var fromSelect = from.isOverlappingWith(pointerObj); - var toSelect = to.isOverlappingWith(pointerObj); + 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; }; })(); - if (fromSelect === true) { - this.selectedControlNode = from; - edge.edgeType.from = from; - } else if (toSelect === true) { - this.selectedControlNode = to; - edge.edgeType.to = to; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var util = __webpack_require__(1); + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(48); + + /** + * clears the toolbar div element of children + * + * @private + */ + + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { + var _this = this; + + _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.touchTime = 0; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.inMode = false; + this.selectedControlNode = undefined; + + 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.body.emitter.emit('_redraw'); - } - }, { - key: '_controlNodeDrag', + 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(ManipulationSystem, [{ + key: '_restore', /** - * dragging the control nodes or the canvas - * @param event + * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. * @private */ - value: function _controlNodeDrag(event) { - this.body.emitter.emit('disablePhysics'); - var pointer = this.body.functions.getPointer(event.center); - var pos = this.canvas.DOMtoCanvas(pointer); - - 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 }; + value: function _restore() { + if (this.inMode !== false) { + if (this.options.initiallyActive === true) { + this.enableEditMode(); + } else { + this.disableEditMode(); + } } - this.body.emitter.emit('_redraw'); } }, { - key: '_controlNodeDragEnd', + key: 'setOptions', /** - * connecting or restoring the control nodes. - * @param event - * @private + * Set the Options + * @param options */ - 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]; - - 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; - } + value: function setOptions(options, allOptions) { + if (allOptions !== undefined) { + if (allOptions.locale !== undefined) { + this.options.locale = allOptions.locale; + }; + if (allOptions.locales !== undefined) { + this.options.locales = allOptions.locales; + }; } - // 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']); + if (options !== undefined) { + if (typeof options === 'boolean') { + this.options.enabled = options; } 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.options.enabled = true; + util.deepExtend(this.options, options); } - } else { - edge.updateEdgeType(); - this.body.emitter.emit('restorePhysics'); + if (this.options.initiallyActive === true) { + this.editMode = true; + } + this._setup(); } - this.body.emitter.emit('_redraw'); } }, { - key: '_handleConnect', - - // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + key: 'toggleEditMode', - // ------------------------------------------- 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. + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. * * @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 - - var pointer = this.lastTouch; - var node = this.selectionHandler.getNodeAt(pointer); - - 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); - - // 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, - dynamic: false, - type: 'continuous', - roundness: 0.5 - } - }); - this.body.edges[connectionEdge.id] = connectionEdge; - this.body.edgeIndices.push(connectionEdge.id); + value: function toggleEditMode() { + if (this.editMode === true) { + this.disableEditMode(); + } else { + this.enableEditMode(); + } + } + }, { + key: 'enableEditMode', + value: function enableEditMode() { + this.editMode = true; - this.temporaryIds.nodes.push(targetNode.id); - this.temporaryIds.edges.push(connectionEdge.id); - } - } - this.touchTime = new Date().valueOf(); + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; + this.editModeDiv.style.display = 'none'; + this.showManipulatorToolbar(); } } }, { - 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: 'disableEditMode', + value: function disableEditMode() { + this.editMode = 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: '_finishConnect', + key: 'showManipulatorToolbar', /** - * Connect the new edge to the target if one exists, otherwise remove temp line - * @param event + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * * @private */ - value: function _finishConnect(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - // remember the edge id - var connectFromId = undefined; - if (this.temporaryIds.edges[0] !== undefined) { - connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; - } + // reset global letiables + this.manipulationDOM = {}; - // 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; - } - } + // 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'; - // clean temporary nodes and edges. - this._cleanupTemporaryNodesAndEdges(); + 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; - // 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); + 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.body.emitter.emit('_redraw'); - } - }, { - key: '_performAddNode', - - // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// - - // ------------------------------ Performing all the actual data manipulation ------------------------// - - /** - * Adds a node on the specified location - */ - value: function _performAddNode(clickData) { - var _this4 = this; - var defaultData = { - id: util.randomUUID(), - x: clickData.pointer.canvas.x, - y: clickData.pointer.canvas.y, - label: 'new' - }; + 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); + } - if (typeof this.options.addNode === 'function') { - if (this.options.addNode.length === 2) { - this.options.addNode(defaultData, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'addNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this4.body.data.nodes.add(finalizedData); - _this4.showManipulatorToolbar(); + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.deleteNode !== false) { + if (needSeperator === true) { + this._createSeperator(4); } - }); - } else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this.showManipulatorToolbar(); - } - } else { - this.body.data.nodes.add(defaultData); - this.showManipulatorToolbar(); + 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)); } + + // redraw to show any possible changes + this.body.emitter.emit('_redraw'); } }, { - key: '_performAddEdge', + key: 'addNodeMode', /** - * connect two nodes with a new edge. + * Create the toolbar for adding Nodes * * @private */ - value: function _performAddEdge(sourceNodeId, targetNodeId) { - var _this5 = this; + value: function addNodeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - 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 && _this5.inMode === 'addEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this5.body.data.edges.add(finalizedData); - _this5.selectionHandler.unselectAll(); - _this5.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(); + // 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: '_performEditEdge', + key: 'editNodeMode', /** - * connect two nodes with a new edge. + * call the bound function to handle the editing of the node. The node has to be selected. * * @private */ - value: function _performEditEdge(sourceNodeId, targetNodeId) { - var _this6 = this; + value: function editNodeMode() { + var _this2 = this; - 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 || _this6.inMode !== 'editEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this6.body.edges[defaultData.id].updateEdgeType(); - _this6.body.emitter.emit('_redraw'); - } else { - _this6.body.data.edges.update(finalizedData); - _this6.selectionHandler.unselectAll(); - _this6.showManipulatorToolbar(); - } - }); + // 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(); + + this.inMode = 'editNode'; + if (typeof this.options.editNode === 'function') { + var node = this.selectionHandler._getSelectedNode(); + 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 === 'delete') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this2.body.data.nodes.update(finalizedData); + _this2.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); + } } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); + alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); } } else { - this.body.data.edges.update(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); + throw new Error('No function has been configured to handle the editing of nodes.'); } } - }]); - - return ManipulationSystem; - })(); - - exports['default'] = ManipulationSystem; - module.exports = exports['default']; - -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + }, { + key: 'addEdgeMode', - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * 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(); + } - var _componentsColorPicker = __webpack_require__(83); + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - var _componentsColorPicker2 = _interopRequireDefault(_componentsColorPicker); + 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']); - var util = __webpack_require__(1); + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - /** - * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. - * Boolean options are recognised as Boolean - * Number options should be written as array: [default value, min value, max value, stepsize] - * Colors should be written as array: ['color', '#ffffff'] - * Strings with should be written as array: [option1, option2, option3, ..] - * - * The options are matched with their counterparts in each of the modules and the values used in the configuration are - * - * @param parentModule | the location where parentModule.setOptions() can be called - * @param defaultContainer | the default container of the module - * @param configureOptions | the fully configured and predefined options set found in allOptions.js - * @param pixelRatio | canvas pixel ratio - */ + // 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)); - var ConfigurationSystem = (function () { - function ConfigurationSystem(parentModule, defaultContainer, configureOptions) { - var pixelRatio = arguments[3] === undefined ? 1 : arguments[3]; + this._temporaryBindUI('onDragStart', function () {}); + this._temporaryBindUI('onHold', function () {}); + } + }, { + key: 'editEdgeMode', - _classCallCheck(this, ConfigurationSystem); + /** + * create the toolbar to edit edges + * + * @private + */ + value: function editEdgeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - this.parent = parentModule; - this.changedOptions = []; - this.container = defaultContainer; + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - this.options = {}; - this.defaultOptions = { - enabled: false, - filter: true, - container: undefined - }; - util.extend(this.options, this.defaultOptions); + 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']); - this.configureOptions = configureOptions; - this.moduleOptions = {}; - this.domElements = []; - this.colorPicker = new _componentsColorPicker2['default'](pixelRatio); - this.wrapper; - } + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - _createClass(ConfigurationSystem, [{ - key: 'setOptions', + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + var edge = this.body.edges[this.edgeBeingEditedId]; - /** - * refresh all options. - * Because all modules parse their options by themselves, we just use their options. We copy them here. - * - * @param options - */ - value: function setOptions(options) { - if (options !== undefined) { - var enabled = true; - if (typeof options === 'string') { - this.options.filter = options; - } else if (options instanceof Array) { - this.options.filter = options.join(); - } else if (typeof options === 'object') { - if (options.container !== undefined) { - this.options.container = options.container; - } - if (options.filter !== undefined) { - this.options.filter = options.filter; - } - if (options.enabled !== undefined) { - enabled = options.enabled; - } - } else if (typeof options === 'boolean') { - this.options.filter = true; - enabled = options; + // create control nodes + var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + + this.temporaryIds.nodes.push(controlNodeFrom.id); + this.temporaryIds.nodes.push(controlNodeTo.id); + + this.body.nodes[controlNodeFrom.id] = controlNodeFrom; + this.body.nodeIndices.push(controlNodeFrom.id); + this.body.nodes[controlNodeTo.id] = controlNodeTo; + this.body.nodeIndices.push(controlNodeTo.id); + + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position + this._temporaryBindUI('onTap', function () {}); // disabled + this._temporaryBindUI('onHold', function () {}); // disabled + this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this)); // used to select control node + this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node + this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes + this._temporaryBindUI('onMouseMove', function () {}); // disabled + + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + this._temporaryBindEvent('beforeDrawing', function (ctx) { + var positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; } - this.options.enabled = enabled; - } - this._clean(); - } - }, { - key: 'setModuleOptions', - value: function setModuleOptions(moduleOptions) { - this.moduleOptions = moduleOptions; - if (this.options.enabled === true) { - this._clean(); - if (this.options.container !== undefined) { - this.container = this.options.container; + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; } - this._create(this.options.filter); - } + }); + + this.body.emitter.emit('_redraw'); } }, { - key: '_create', + key: 'deleteSelected', /** - * Create all DOM elements - * @param {Boolean | String} config + * delete everything in the selection + * * @private */ - value: function _create(config) { - var _this = this; - - this._clean(); - this.changedOptions = []; + value: function deleteSelected() { + var _this3 = this; - var counter = 0; - for (var option in this.configureOptions) { - if (this.configureOptions.hasOwnProperty(option)) { - if (config === true || config.indexOf(option) !== -1) { - var optionObj = this.configureOptions[option]; + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - // linebreak between categories - if (counter > 0) { - this._makeItem([]); - } - // a header for the category - this._makeHeader(option); + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - // get the suboptions - var path = [option]; - this._handleObject(optionObj, path); + 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; } - counter++; } - } - var generateButton = document.createElement('div'); - generateButton.className = 'vis-network-configuration button'; - generateButton.innerHTML = 'generate options'; - generateButton.onclick = function () { - _this._printOptions(); - }; - generateButton.onmouseover = function () { - generateButton.className = 'vis-network-configuration button hover'; - }; - generateButton.onmouseout = function () { - generateButton.className = 'vis-network-configuration button'; - }; - - this.optionsContainer = document.createElement('div'); - this.optionsContainer.className = 'vis-network-configuration vis-option-container'; - this.domElements.push(this.optionsContainer); - this.domElements.push(generateButton); + if (typeof this.options.deleteNode === 'function') { + deleteFunction = this.options.deleteNode; + } + } else if (selectedEdges.length > 0) { + if (typeof this.options.deleteEdge === 'function') { + deleteFunction = this.options.deleteEdge; + } + } - this._push(); - this.colorPicker.insertTo(this.container); + if (typeof deleteFunction === 'function') { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length === 2) { + deleteFunction(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this3.inMode === 'delete') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this3.body.data.edges.remove(finalizedData.edges); + _this3.body.data.nodes.remove(finalizedData.nodes); + _this3.body.emitter.emit('startSimulation'); + } + }); + } else { + throw new Error('The function for delete does not support two arguments (data, callback)'); + } + } else { + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this.body.emitter.emit('startSimulation'); + } } }, { - key: '_push', + key: '_setup', + + //********************************************** PRIVATE ***************************************// /** - * draw all DOM elements on the screen + * draw or remove the DOM * @private */ - value: function _push() { - this.wrapper = document.createElement('div'); - this.wrapper.className = 'vis-network-configuration-wrapper'; - this.container.appendChild(this.wrapper); - for (var i = 0; i < this.domElements.length; i++) { - this.wrapper.appendChild(this.domElements[i]); + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; + + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); + } + } else { + this._removeManipulationDOM(); + + // disable the gui + this.guiEnabled = false; } } }, { - key: '_clean', + key: '_createWrappers', /** - * delete all DOM elements + * create the div overlays that contain the DOM * @private */ - value: function _clean() { - for (var i = 0; i < this.domElements.length; i++) { - this.wrapper.removeChild(this.domElements[i]); + 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); } - if (this.wrapper !== undefined) { - this.container.removeChild(this.wrapper); - this.wrapper = undefined; + // 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); + } + + // 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); } - this.domElements = []; } }, { - key: '_getValue', + key: '_getNewTargetNode', /** - * get the value from the actualOptions if it exists - * @param {array} path | where to look for the actual option + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y * @returns {*} * @private */ - value: function _getValue(path) { - var base = this.moduleOptions; - for (var i = 0; i < path.length; i++) { - if (base[path[i]] !== undefined) { - base = base[path[i]]; - } else { - base = undefined; - break; - } - } - return base; + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + + controlNodeStyle.id = 'targetNode' + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; + + return this.body.functions.createNode(controlNodeStyle); + } + }, { + key: '_createEditButton', + + /** + * Create the edit button + */ + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); + + // reset the manipulationDOM + this.manipulationDOM = {}; + + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); + + // 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); + + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); } }, { - key: '_makeItem', + key: '_clean', /** - * all option elements are wrapped in an item - * @param path - * @param domElements + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. * @private */ - value: function _makeItem(path) { - for (var _len = arguments.length, domElements = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - domElements[_key - 1] = arguments[_key]; + 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(); } - var item = document.createElement('div'); - item.className = 'vis-network-configuration item s' + path.length; - domElements.forEach(function (element) { - item.appendChild(element); - }); - this.domElements.push(item); - } - }, { - key: '_makeHeader', + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - /** - * header for major subjects - * @param name - * @private - */ - value: function _makeHeader(name) { - var div = document.createElement('div'); - div.className = 'vis-network-configuration header'; - div.innerHTML = name; - this._makeItem([], div); + // restore overloaded UI functions + this._unbindTemporaryUIs(); + + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); + + // restore the physics if required + this.body.emitter.emit('restorePhysics'); } }, { - key: '_makeLabel', + key: '_cleanManipulatorHammers', /** - * make a label, if it is an object label, it gets different styling. - * @param name - * @param path - * @param objectLabel - * @returns {HTMLElement} + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. * @private */ - value: function _makeLabel(name, path) { - var objectLabel = arguments[2] === undefined ? false : arguments[2]; - - var div = document.createElement('div'); - div.className = 'vis-network-configuration label s' + path.length; - if (objectLabel === true) { - div.innerHTML = '' + name + ':'; - } else { - div.innerHTML = name + ':'; + 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 = []; } - return div; } }, { - key: '_makeDropdown', + key: '_removeManipulationDOM', /** - * make a dropdown list for multiple possible string optoins - * @param arr - * @param value - * @param path + * Remove all DOM elements created by this module. * @private */ - value: function _makeDropdown(arr, value, path) { - var select = document.createElement('select'); - select.className = 'vis-network-configuration select'; - var selectedValue = 0; - if (value !== undefined) { - if (arr.indexOf(value) !== -1) { - selectedValue = arr.indexOf(value); - } - } + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - for (var i = 0; i < arr.length; i++) { - var option = document.createElement('option'); - option.value = arr[i]; - if (i === selectedValue) { - option.selected = 'selected'; - } - option.innerHTML = arr[i]; - select.appendChild(option); - } + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); - var me = this; - select.onchange = function () { - me._update(this.value, path); - }; + // remove the manipulation divs + this.canvas.frame.removeChild(this.manipulationDiv); + this.canvas.frame.removeChild(this.editModeDiv); + this.canvas.frame.removeChild(this.closeDiv); - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, select); + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; } }, { - key: '_makeRange', + key: '_createSeperator', /** - * make a range object for numeric options - * @param arr - * @param value - * @param path + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index * @private */ - value: function _makeRange(arr, value, path) { - var defaultValue = arr[0]; - var min = arr[1]; - var max = arr[2]; - var step = arr[3]; - var range = document.createElement('input'); - range.type = 'range'; - range.className = 'vis-network-configuration range'; - range.min = min; - range.max = max; - range.step = step; - - if (value !== undefined) { - if (value * 0.1 < min) { - range.min = value / 10; - } - if (value * 2 > max && max !== 1) { - range.max = value * 2; - } - range.value = value; - } else { - range.value = defaultValue; - } - - var input = document.createElement('input'); - input.className = 'vis-network-configuration rangeinput'; - input.value = range.value; - - var me = this; - range.onchange = function () { - input.value = this.value;me._update(Number(this.value), path); - }; - range.oninput = function () { - input.value = this.value; - }; + value: function _createSeperator() { + var index = arguments[0] === undefined ? 1 : arguments[0]; - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, range, input); + this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); + this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; + this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); } }, { - key: '_makeCheckbox', + key: '_createAddNodeButton', - /** - * make a checkbox for boolean options. - * @param defaultValue - * @param value - * @param path - * @private - */ - value: function _makeCheckbox(defaultValue, value, path) { - var checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'vis-network-configuration checkbox'; - checkbox.checked = defaultValue; - if (value !== undefined) { - checkbox.checked = value; - if (value !== defaultValue) { - if (typeof defaultValue === 'object') { - if (value !== defaultValue.enabled) { - this.changedOptions.push({ path: path, value: value }); - } - } else { - this.changedOptions.push({ path: path, value: value }); - } - } - } + // ---------------------- DOM functions for buttons --------------------------// - var me = this; - checkbox.onchange = function () { - me._update(this.checked, path); - }; + 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('editNodeMode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editNodeMode.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]; - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, checkbox); + 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)); } }, { - key: '_makeTextInput', + key: '_temporaryBindEvent', + + // -------------------------- End of DOM functions for buttons ------------------------------// /** - * make a text input field for string options. - * @param defaultValue - * @param value - * @param path + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction * @private */ - value: function _makeTextInput(defaultValue, value, path) { - var checkbox = document.createElement('input'); - checkbox.type = 'text'; - checkbox.className = 'vis-network-configuration text'; - checkbox.value = value; - if (value !== defaultValue) { - this.changedOptions.push({ path: path, value: value }); - } - - var me = this; - checkbox.onchange = function () { - me._update(this.value, path); - }; - - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, checkbox); + value: function _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); + this.body.emitter.on(event, newFunction); } }, { - key: '_makeColorField', + key: '_temporaryBindUI', /** - * make a color field with a color picker for color fields - * @param arr - * @param value - * @param path + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction * @private */ - value: function _makeColorField(arr, value, path) { - var _this2 = this; - - var defaultColor = arr[1]; - var div = document.createElement('div'); - value = value === undefined ? defaultColor : value; - - if (value !== 'none') { - div.className = 'vis-network-configuration colorBlock'; - div.style.backgroundColor = value; + value: function _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; } else { - div.className = 'vis-network-configuration colorBlock none'; + throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); } - - value = value === undefined ? defaultColor : value; - div.onclick = function () { - _this2._showColorPicker(value, div, path); - }; - - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, div); } }, { - key: '_showColorPicker', + key: '_unbindTemporaryUIs', /** - * used by the color buttons to call the color picker. - * @param event - * @param value - * @param div - * @param path + * Restore the overridden UI functions to their original state. + * * @private */ - value: function _showColorPicker(value, div, path) { - var _this3 = this; - - var rect = div.getBoundingClientRect(); - var bodyRect = document.body.getBoundingClientRect(); - var pickerX = rect.left + rect.width + 5; - var pickerY = rect.top - bodyRect.top + rect.height * 0.5; - this.colorPicker.show(pickerX, pickerY); - this.colorPicker.setColor(value); - this.colorPicker.setCallback(function (color) { - var colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')'; - div.style.backgroundColor = colorString; - _this3._update(colorString, path); - }); + 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: '_handleObject', + key: '_unbindTemporaryEvents', /** - * parse an object and draw the correct items - * @param obj - * @param path + * Unbind the events created by _temporaryBindEvent * @private */ - value: function _handleObject(obj) { - var path = arguments[1] === undefined ? [] : arguments[1]; - - for (var subObj in obj) { - if (obj.hasOwnProperty(subObj)) { - var item = obj[subObj]; - var newPath = util.copyAndExtendArray(path, subObj); - var value = this._getValue(newPath); - - if (item instanceof Array) { - this._handleArray(item, value, newPath); - } else if (typeof item === 'string') { - this._makeTextInput(item, value, newPath); - } else if (typeof item === 'boolean') { - this._makeCheckbox(item, value, newPath); - } else if (item instanceof Object) { - // collapse the physics options that are not enabled - var draw = true; - if (path.indexOf('physics') !== -1) { - if (this.moduleOptions.physics.solver !== subObj) { - draw = false; - } - } - - if (draw === true) { - // initially collapse options with an disabled enabled option. - if (item.enabled !== undefined) { - var enabledPath = util.copyAndExtendArray(newPath, 'enabled'); - var enabledValue = this._getValue(enabledPath); - if (enabledValue === true) { - var label = this._makeLabel(subObj, newPath, true); - this._makeItem(newPath, label); - this._handleObject(item, newPath); - } else { - this._makeCheckbox(item, enabledValue, newPath); - } - } else { - var label = this._makeLabel(subObj, newPath, true); - this._makeItem(newPath, label); - this._handleObject(item, newPath); - } - } - } else { - console.error('dont know how to handle', item, subObj, newPath); - } - } + 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: '_handleArray', + key: '_bindHammerToDiv', /** - * handle the array type of option - * @param optionName - * @param arr - * @param value - * @param path - * @private + * Bind an hammer instance to a DOM element. + * @param domElement + * @param funct */ - value: function _handleArray(arr, value, path) { - if (typeof arr[0] === 'string' && arr[0] === 'color') { - this._makeColorField(arr, value, path); - if (arr[1] !== value) { - this.changedOptions.push({ path: path, value: value }); - } - } else if (typeof arr[0] === 'string') { - this._makeDropdown(arr, value, path); - if (arr[0] !== value) { - this.changedOptions.push({ path: path, value: value }); - } - } else if (typeof arr[0] === 'number') { - this._makeRange(arr, value, path); - if (arr[0] !== value) { - this.changedOptions.push({ path: path, value: Number(value) }); - } - } + value: function _bindHammerToDiv(domElement, boundFunction) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, boundFunction); + this.manipulationHammers.push(hammer); } }, { - key: '_update', + key: '_cleanupTemporaryNodesAndEdges', /** - * called to update the network with the new settings. - * @param value - * @param path + * Neatly clean up temporary edges and nodes * @private */ - value: function _update(value, path) { - var options = this._constructOptions(value, path); - this.parent.setOptions(options); - } - }, { - key: '_constructOptions', - value: function _constructOptions(value, path) { - var optionsObj = arguments[2] === undefined ? {} : arguments[2]; - - var pointer = optionsObj; - - // when dropdown boxes can be string or boolean, we typecast it into correct types - value = value === 'true' ? true : value; - value = value === 'false' ? false : value; - - for (var i = 0; i < path.length; i++) { - if (path[i] !== 'global') { - if (pointer[path[i]] === undefined) { - pointer[path[i]] = {}; - } - if (i !== path.length - 1) { - pointer = pointer[path[i]]; - } else { - pointer[path[i]] = value; - } - } - } - return optionsObj; - } - }, { - key: '_printOptions', - value: function _printOptions() { - var options = {}; - for (var i = 0; i < this.changedOptions.length; i++) { - this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options); - } - this.optionsContainer.innerHTML = '
var options = ' + JSON.stringify(options, null, 2) + '
'; - } - }]); - - return ConfigurationSystem; - })(); - - exports['default'] = ConfigurationSystem; - module.exports = exports['default']; - -/***/ }, -/* 63 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(1); + 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); + } + } - var errorFound = false; - var allOptions = undefined; - var printStyle = 'background: #FFeeee; color: #dd0000'; - /** - * Used to validate options. - */ + // _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 Validator = (function () { - function Validator() { - _classCallCheck(this, Validator); - } + this.temporaryIds = { nodes: [], edges: [] }; + } + }, { + key: '_controlNodeTouch', - _createClass(Validator, null, [{ - key: 'validate', + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// /** - * Main function to be called - * @param options - * @param subObject - * @returns {boolean} + * the touch is used to get the position of the initial click + * @param event + * @private */ - value: function validate(options, referenceOptions, subObject) { - errorFound = false; - allOptions = referenceOptions; - var usedOptions = referenceOptions; - if (subObject !== undefined) { - usedOptions = referenceOptions[subObject]; - } - Validator.parse(options, usedOptions, []); - return errorFound; + 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: 'parse', + key: '_controlNodeDragStart', /** - * Will traverse an object recursively and check every value - * @param options - * @param referenceOptions - * @param path + * the drag start is used to mark one of the control nodes as selected. + * @param event + * @private */ - value: function parse(options, referenceOptions, path) { - for (var option in options) { - if (options.hasOwnProperty(option)) { - Validator.check(option, options, referenceOptions, path); - } + 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 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: 'check', + key: '_controlNodeDrag', /** - * Check every value. If the value is an object, call the parse function on that object. - * @param option - * @param options - * @param referenceOptions - * @param path + * dragging the control nodes or the canvas + * @param event + * @private */ - value: function check(option, options, referenceOptions, path) { - if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) { - Validator.getSuggestion(option, referenceOptions, path); - } else if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) { - // __any__ is a wildcard. Any value is accepted and will be further analysed by reference. - if (Validator.getType(options[option]) === 'object') { - Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'].__type__, path); - } + value: function _controlNodeDrag(event) { + this.body.emitter.emit('disablePhysics'); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); + + if (this.selectedControlNode !== undefined) { + this.selectedControlNode.x = pos.x; + this.selectedControlNode.y = pos.y; } else { - // Since all options in the reference are objects, we can check whether they are supposed to be object to look for the __type__ field. - if (referenceOptions[option].__type__ !== undefined) { - // if this should be an object, we check if the correct type has been supplied to account for shorthand options. - Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option].__type__, path); - } else { - Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option], path); - } + // 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: 'checkFields', + key: '_controlNodeDragEnd', /** - * - * @param {String} option | the option property - * @param {Object} options | The supplied options object - * @param {Object} referenceOptions | The reference options containing all options and their allowed formats - * @param {String} referenceOption | Usually this is the same as option, except when handling an __any__ tag. - * @param {String} refOptionType | This is the type object from the reference options - * @param {Array} path | where in the object is the option + * connecting or restoring the control nodes. + * @param event + * @private */ - value: function checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) { - var optionType = Validator.getType(options[option]); - var refOptionType = refOptionObj[optionType]; - if (refOptionType !== undefined) { - // if the type is correct, we check if it is supposed to be one of a few select values - if (Validator.getType(refOptionType) === 'array') { - if (refOptionType.indexOf(options[option]) === -1) { - console.log('%cInvalid option detected in "' + option + '".' + ' Allowed values are:' + Validator.print(refOptionType) + ' not "' + options[option] + '". ' + Validator.printLocation(path, option), printStyle); - errorFound = true; - } else if (optionType === 'object') { - path = util.copyAndExtendArray(path, option); - Validator.parse(options[option], referenceOptions[referenceOption], path); + 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]; + + 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; + } + } + + // 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 if (optionType === 'object') { - path = util.copyAndExtendArray(path, option); - Validator.parse(options[option], referenceOptions[referenceOption], path); } } else { - if (refOptionObj['undef'] !== undefined && optionType === 'undefined') {} else if (refOptionObj['fn'] !== undefined && optionType === 'function') {} else { - // type of the field is incorrect - console.log('%cInvalid type received for "' + option + '". Expected: ' + Validator.print(Object.keys(refOptionObj)) + '. Received [' + optionType + '] "' + options[option] + '"' + Validator.printLocation(path, option), printStyle); - errorFound = true; - } + edge.updateEdgeType(); + this.body.emitter.emit('restorePhysics'); } + this.body.emitter.emit('_redraw'); } }, { - key: 'getType', - value: function getType(object) { - var type = typeof object; + key: '_handleConnect', - if (type === 'object') { - if (object === null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'boolean'; - } - if (object instanceof Number) { - return 'number'; - } - if (object instanceof String) { - return 'string'; - } - if (Array.isArray(object)) { - return 'array'; - } - if (object instanceof Date) { - return 'date'; - } - if (object.nodeType !== undefined) { - return 'dom'; - } - if (object._isAMomentObject === true) { - return 'moment'; + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + + // ------------------------------------------- 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 + + var pointer = this.lastTouch; + var node = this.selectionHandler.getNodeAt(pointer); + + 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); + + // 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, + dynamic: false, + type: 'continuous', + roundness: 0.5 + } + }); + this.body.edges[connectionEdge.id] = connectionEdge; + this.body.edgeIndices.push(connectionEdge.id); + + this.temporaryIds.nodes.push(targetNode.id); + this.temporaryIds.edges.push(connectionEdge.id); + } } - return 'object'; - } else if (type === 'number') { - return 'number'; - } else if (type === 'boolean') { - return 'boolean'; - } else if (type === 'string') { - return 'string'; - } else if (type === undefined) { - return 'undefined'; + this.touchTime = new Date().valueOf(); } - return type; } }, { - key: 'getSuggestion', - value: function getSuggestion(option, options, path) { - var localSearch = Validator.findInOptions(option, options, path, false); - var globalSearch = Validator.findInOptions(option, allOptions, [], true); - - var localSearchThreshold = 8; - var globalSearchThreshold = 5; - - if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) { - console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was misplaced? Matching option found at: ' + Validator.printLocation(globalSearch.path, option, ''), printStyle); - } else if (localSearch.distance <= localSearchThreshold) { - console.log('%cUnknown option detected: "' + option + '". Did you mean "' + localSearch.closestMatch + '"?' + Validator.printLocation(localSearch.path, option), printStyle); + 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 { - console.log('%cUnknown option detected: "' + option + '". Did you mean one of these: ' + Validator.print(Object.keys(options)) + Validator.printLocation(path, option), printStyle); + 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 }; } - - errorFound = true; } }, { - key: 'findInOptions', + key: '_finishConnect', /** - * traverse the options in search for a match. - * @param option - * @param options - * @param path - * @param recursive - * @returns {{closestMatch: string, path: Array, distance: number}} + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event + * @private */ - value: function findInOptions(option, options, path) { - var recursive = arguments[3] === undefined ? false : arguments[3]; + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var min = 1000000000; - var closestMatch = ''; - var closestMatchPath = []; - for (var op in options) { - var type = Validator.getType(options[op]); - var distance = undefined; - if (type === 'object' && recursive === true) { - var result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path, op)); - if (min > result.distance) { - closestMatch = result.closestMatch; - closestMatchPath = result.path; - min = result.distance; - } + // remember the edge id + var connectFromId = undefined; + if (this.temporaryIds.edges[0] !== undefined) { + connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; + } + + // 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; + } + } + + // clean temporary nodes and edges. + this._cleanupTemporaryNodesAndEdges(); + + // perform the connection + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); } else { - distance = Validator.levenshteinDistance(option, op); - if (min > distance) { - closestMatch = op; - closestMatchPath = util.copyArray(path); - min = distance; + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performAddEdge(connectFromId, node.id); } } } - return { closestMatch: closestMatch, path: closestMatchPath, distance: min }; + this.body.emitter.emit('_redraw'); } }, { - key: 'printLocation', - value: function printLocation(path, option) { - var prefix = arguments[2] === undefined ? 'Problem value found at: \n' : arguments[2]; + key: '_performAddNode', - var str = '\n\n' + prefix + 'options = {\n'; - for (var i = 0; i < path.length; i++) { - for (var j = 0; j < i + 1; j++) { - str += ' '; - } - str += path[i] + ': {\n'; - } - for (var j = 0; j < path.length + 1; j++) { - str += ' '; - } - str += option + '\n'; - for (var i = 0; i < path.length + 1; i++) { - for (var j = 0; j < path.length - i; j++) { - str += ' '; + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + + // ------------------------------ Performing all the actual data manipulation ------------------------// + + /** + * Adds a node on the specified location + */ + value: function _performAddNode(clickData) { + var _this4 = this; + + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: 'new' + }; + + if (typeof this.options.addNode === 'function') { + if (this.options.addNode.length === 2) { + this.options.addNode(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'addNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this4.body.data.nodes.add(finalizedData); + _this4.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for add does not support two arguments (data,callback)'); + this.showManipulatorToolbar(); } - str += '}\n'; + } else { + this.body.data.nodes.add(defaultData); + this.showManipulatorToolbar(); } - return str + '\n\n'; } }, { - key: 'print', - value: function print(options) { - return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, '').replace(/(\,)/g, ', '); - } - }, { - key: 'levenshteinDistance', + key: '_performAddEdge', - // Compute the edit distance between the two given strings - // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript - /* - Copyright (c) 2011 Andrei Mackenzie - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + /** + * connect two nodes with a new edge. + * + * @private */ - value: function levenshteinDistance(a, b) { - if (a.length === 0) return b.length; - if (b.length === 0) return a.length; - - var matrix = []; + value: function _performAddEdge(sourceNodeId, targetNodeId) { + var _this5 = this; - // increment along the first column of each row - var i; - for (i = 0; i <= b.length; i++) { - matrix[i] = [i]; + 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 && _this5.inMode === 'addEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this5.body.data.edges.add(finalizedData); + _this5.selectionHandler.unselectAll(); + _this5.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', - // increment each column in the first row - var j; - for (j = 0; j <= a.length; j++) { - matrix[0][j] = j; - } + /** + * connect two nodes with a new edge. + * + * @private + */ + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this6 = this; - // Fill in the rest of the matrix - for (i = 1; i <= b.length; i++) { - for (j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) == a.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution - Math.min(matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j] + 1)); // deletion - } + 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 || _this6.inMode !== 'editEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this6.body.edges[defaultData.id].updateEdgeType(); + _this6.body.emitter.emit('_redraw'); + } else { + _this6.body.data.edges.update(finalizedData); + _this6.selectionHandler.unselectAll(); + _this6.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); } + } else { + this.body.data.edges.update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); } - - return matrix[b.length][a.length]; } }]); - return Validator; + return ManipulationSystem; })(); - exports['default'] = Validator; - exports.printStyle = printStyle; - - // item is undefined, which is allowed - - // item is a function, which is allowed + exports['default'] = ManipulationSystem; + module.exports = exports['default']; /***/ }, -/* 64 */ +/* 65 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -26993,7 +27254,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.configureOptions = configureOptions; /***/ }, -/* 65 */ +/* 66 */ /***/ function(module, exports, __webpack_require__) { /** @@ -27277,12 +27538,12 @@ return /******/ (function(modules) { // webpackBootstrap } /***/ }, -/* 66 */ +/* 67 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var keycharm = __webpack_require__(84); + var keycharm = __webpack_require__(85); var Emitter = __webpack_require__(43); var Hammer = __webpack_require__(41); var util = __webpack_require__(1); @@ -27430,7 +27691,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Activator; /***/ }, -/* 67 */ +/* 68 */ /***/ function(module, exports, __webpack_require__) { // English @@ -27474,7 +27735,7 @@ return /******/ (function(modules) { // webpackBootstrap exports['nl_BE'] = exports['nl']; /***/ }, -/* 68 */ +/* 69 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(module) {//! moment.js @@ -29595,3672 +29856,4252 @@ return /******/ (function(modules) { // webpackBootstrap return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); } - addFormatToken('Q', 0, 0, 'quarter'); + addFormatToken('Q', 0, 0, 'quarter'); + + // ALIASES + + addUnitAlias('quarter', 'Q'); + + // PARSING + + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); + + // MOMENTS + + function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + } + + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); + + // MOMENTS + + var getSetDayOfMonth = makeGetSet('Date', true); + + addFormatToken('d', 0, 'do', 'day'); + + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); + + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); + + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); + + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); + + // ALIASES + + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); + + // PARSING + + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { + var weekday = config._locale.weekdaysParse(input); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + config._pf.invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = locale.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } + + // LOCALES + + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m) { + return this._weekdays[m.day()]; + } + + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } + + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } + + function localeWeekdaysParse (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = local__createLocal([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + } + + // MOMENTS + + function getSetDayOfWeek (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + } + + function getSetLocaleDayOfWeek (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } + + function getSetISODayOfWeek (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + } + + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, function () { + return this.hours() % 12 || 12; + }); + + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); + } + + meridiem('a', true); + meridiem('A', false); + + // ALIASES + + addUnitAlias('hour', 'h'); + + // PARSING + + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; + } + + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); + + addParseToken(['H', 'HH'], HOUR); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + config._pf.bigHour = true; + }); + + // LOCALES + + function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + } + + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } + + + // MOMENTS + + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + var getSetHour = makeGetSet('Hours', true); + + addFormatToken('m', ['mm', 2], 0, 'minute'); + + // ALIASES + + addUnitAlias('minute', 'm'); + + // PARSING + + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); + + // MOMENTS + + var getSetMinute = makeGetSet('Minutes', false); + + addFormatToken('s', ['ss', 2], 0, 'second'); // ALIASES - addUnitAlias('quarter', 'Q'); + addUnitAlias('second', 's'); // PARSING - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); // MOMENTS - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + var getSetSecond = makeGetSet('Seconds', false); + + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); + }); + + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); + + function millisecond__milliseconds (token) { + addFormatToken(0, [token, 3], 0, 'millisecond'); } - addFormatToken('D', ['DD', 2], 'Do', 'date'); + millisecond__milliseconds('SSS'); + millisecond__milliseconds('SSSS'); // ALIASES - addUnitAlias('date', 'D'); + addUnitAlias('millisecond', 'ms'); // PARSING - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + addRegexToken('SSSS', matchUnsigned); + addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); }); - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); - }); + // MOMENTS + + var getSetMillisecond = makeGetSet('Milliseconds', false); + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); // MOMENTS - var getSetDayOfMonth = makeGetSet('Date', true); + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; + } - addFormatToken('d', 0, 'do', 'day'); + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); + var momentPrototype__proto = Moment.prototype; - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = moment_format__toISOString; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; - // ALIASES + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; - // PARSING + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { - var weekday = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (weekday != null) { - week.d = weekday; - } else { - config._pf.invalidWeekday = input; - } - }); + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - // HELPERS + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - // LOCALES + // Offset + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; + // Deprecations + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + + var momentPrototype = momentPrototype__proto; + + function moment__createUnix (input) { + return local__createLocal(input * 1000); } - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); } - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; + var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }; - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.call(mom, now) : output; + } - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = local__createLocal([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } + var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY LT', + LLLL : 'dddd, MMMM D, YYYY LT' + }; + + function longDateFormat (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; } + return output; } - // MOMENTS + var defaultInvalidDate = 'Invalid date'; - function getSetDayOfWeek (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } + function invalidDate () { + return this._invalidDate; } - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); } - function getSetISODayOfWeek (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + function preParsePostFormat (string) { + return string; } - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); + var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }; - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); + function relative__relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); } - meridiem('a', true); - meridiem('A', false); + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + } - // ALIASES + function locale_set__set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source); + } - addUnitAlias('hour', 'h'); + var prototype__proto = Locale.prototype; - // PARSING + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._longDateFormat = defaultLongDateFormat; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - config._pf.bigHour = true; - }); + // Day of Week + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; + prototype__proto.weekdaysParse = localeWeekdaysParse; - // LOCALES + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; - function localeIsPM (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); + function lists__get (format, index, field, setter) { + var locale = locale_locales__getLocale(); + var utc = create_utc__createUTC().set(setter, index); + return locale[field](utc, format); } - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; } - } + format = format || ''; - // MOMENTS + if (index != null) { + return lists__get(format, index, field, setter); + } - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; + } - addFormatToken('m', ['mm', 2], 0, 'minute'); + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } - // ALIASES + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } - addUnitAlias('minute', 'm'); + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } - // PARSING + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); + } - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); + } - // MOMENTS + locale_locales__getSetGlobalLocale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); - var getSetMinute = makeGetSet('Minutes', false); + // Side effect imports + utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); + utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - addFormatToken('s', ['ss', 2], 0, 'second'); + var mathAbs = Math.abs; - // ALIASES + function duration_abs__abs () { + var data = this._data; - addUnitAlias('second', 's'); + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); - // PARSING + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); + return this; + } - // MOMENTS + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); - var getSetSecond = makeGetSet('Seconds', false); + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); + return duration._bubble(); + } - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); + // supports only 2.0-style add(1, 's') or add(duration) + function duration_add_subtract__add (input, value) { + return duration_add_subtract__addSubtract(this, input, value, 1); + } - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function duration_add_subtract__subtract (input, value) { + return duration_add_subtract__addSubtract(this, input, value, -1); } - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years = 0; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - // ALIASES + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - addUnitAlias('millisecond', 'ms'); + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - // PARSING + hours = absFloor(minutes / 60); + data.hours = hours % 24; - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); - addRegexToken('SSSS', matchUnsigned); - addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); - }); + days += absFloor(hours / 24); - // MOMENTS + // Accurately convert days to years, assume start from year 0. + years = absFloor(daysToYears(days)); + days -= absFloor(yearsToDays(years)); - var getSetMillisecond = makeGetSet('Milliseconds', false); + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absFloor(days / 30); + days %= 30; - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); + // 12 months -> 1 year + years += absFloor(months / 12); + months %= 12; - // MOMENTS + data.days = days; + data.months = months; + data.years = years; - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; + return this; } - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; } - var momentPrototype__proto = Moment.prototype; - - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = moment_format__toISOString; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; + function yearsToDays (years) { + // years * 365 + absFloor(years / 4) - + // absFloor(years / 100) + absFloor(years / 400); + return years * 146097 / 400; + } - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + units = normalizeUnits(units); - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToYears(days) * 12; + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(yearsToDays(this._months / 12)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 24 * 60 + milliseconds / 6e4; + case 'second' : return days * 24 * 60 * 60 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + } - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; + var asMilliseconds = makeAs('ms'); + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); + } - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + var duration_get__milliseconds = makeGetter('milliseconds'); + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + function weeks () { + return absFloor(this.days() / 7); + } - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; + var round = Math.round; + var thresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }; - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } - // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); - momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + var duration = create__createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); - var momentPrototype = momentPrototype__proto; + var a = seconds < thresholds.s && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours === 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days === 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months === 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years === 1 && ['y'] || ['yy', years]; - function moment__createUnix (input) { - return local__createLocal(input * 1000); + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); } - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); + // This function allows you to set a threshold for relative time strings + function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + return true; } - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); } - var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }; + var iso_string__abs = Math.abs; - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - } + function iso_string__toISOString() { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = iso_string__abs(this.years()); + var M = iso_string__abs(this.months()); + var D = iso_string__abs(this.days()); + var h = iso_string__abs(this.hours()); + var m = iso_string__abs(this.minutes()); + var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); + var total = this.asSeconds(); - var defaultInvalidDate = 'Invalid date'; + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - function invalidDate () { - return this._invalidDate; + return (total < 0 ? '-' : '') + + 'P' + + (Y ? Y + 'Y' : '') + + (M ? M + 'M' : '') + + (D ? D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? h + 'H' : '') + + (m ? m + 'M' : '') + + (s ? s + 'S' : ''); } - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; + var duration_prototype__proto = Duration.prototype; - function ordinal (number) { - return this._ordinal.replace('%d', number); - } + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = duration_get__milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; - function preParsePostFormat (string) { - return string; - } + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; - var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }; + // Side effect imports - function relative__relativeTime (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); - } + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - } + // PARSING - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source); - } + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); - var prototype__proto = Locale.prototype; + // Side effect imports - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; - prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + utils_hooks__hooks.version = '2.10.2'; - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + setHookCallback(local__createLocal); - // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; - prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; + var _moment = utils_hooks__hooks; - function lists__get (format, index, field, setter) { - var locale = locale_locales__getLocale(); - var utc = create_utc__createUTC().set(setter, index); - return locale[field](utc, format); - } + return _moment; - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; - } + })); + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(87)(module))) - format = format || ''; +/***/ }, +/* 70 */ +/***/ function(module, exports, __webpack_require__) { - if (index != null) { - return lists__get(format, index, field, setter); - } + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + + (function (factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + window.propagating = factory(); + } + }(function () { + var _firstTarget = null; // singleton, will contain the target element where the touch event started + var _processing = false; // singleton, true when a touch event is being handled - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); - } - return out; + /** + * Extend an Hammer.js instance with event propagation. + * + * Features: + * - Events emitted by hammer will propagate in order from child to parent + * elements. + * - Events are extended with a function `event.stopPropagation()` to stop + * propagation to parent elements. + * - An option `preventDefault` to stop all default browser behavior. + * + * Usage: + * var hammer = propagatingHammer(new Hammer(element)); + * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); + * + * @param {Hammer.Manager} hammer An hammer instance. + * @param {Object} [options] Available options: + * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. + * Enforce preventing the default browser behavior. + * Cannot be set to `false`. + * @return {Hammer.Manager} Returns the same hammer instance with extended + * functionality + */ + return function propagating(hammer, options) { + if (options && options.preventDefault === false) { + throw new Error('Only supports preventDefault == true'); } + var _options = options || { + preventDefault: false + }; - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); - } + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); - } + var PropagatingHammer = function(element, options) { + return propagating(new Hammer(element, options), _options); + }; + Hammer.extend(PropagatingHammer, Hammer); + PropagatingHammer.Manager = function (element, options) { + return propagating(new Hammer.Manager(element, options), _options); + }; - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); + return PropagatingHammer; } - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); - } + // attach to DOM element + var element = hammer.element; + element.hammer = hammer; - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); - } + // move the original functions that we will wrap + hammer._on = hammer.on; + hammer._off = hammer.off; + hammer._emit = hammer.emit; + hammer._destroy = hammer.destroy; - locale_locales__getSetGlobalLocale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } + /** @type {Object.>} */ + hammer._handlers = {}; + + // register an event to catch the start of a gesture and store the + // target in a singleton + hammer._on('hammer.input', function (event) { + if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { + event.preventDefault(); + } + if (event.isFirst) { + _firstTarget = event.target; + _processing = true; + } + if (event.isFinal) { + _processing = false; + } }); - // Side effect imports - utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); - utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); + /** + * Register a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} handler A callback function, called as handler(event) + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.on = function (events, handler) { + // register the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (!_handlers) { + hammer._handlers[event] = _handlers = []; - var mathAbs = Math.abs; + // register the static, propagated handler + hammer._on(event, propagatedHandler); + } + _handlers.push(handler); + }); - function duration_abs__abs () { - var data = this._data; + return hammer; + }; - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + /** + * Unregister a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} [handler] Optional. The registered handler. If not + * provided, all handlers for given events + * are removed. + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.off = function (events, handler) { + // unregister the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (_handlers) { + _handlers = handler ? _handlers.filter(function (h) { + return h !== handler; + }) : []; - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); + if (_handlers.length > 0) { + hammer._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer._off(event, propagatedHandler); + delete hammer._handlers[event]; + } + } + }); - return this; - } + return hammer; + }; - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + hammer.emit = function(eventType, event) { + if (!_processing) { + _firstTarget = event.target; + } + hammer._emit(eventType, event); + }; - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; + hammer.destroy = function () { + // Detach from DOM element + var element = hammer.element; + delete element.hammer; - return duration._bubble(); - } + // clear all handlers + hammer._handlers = {}; - // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { - return duration_add_subtract__addSubtract(this, input, value, 1); - } + // call original hammer destroy + hammer._destroy(); + }; - // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); } - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; - - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; - - hours = absFloor(minutes / 60); - data.hours = hours % 24; - - days += absFloor(hours / 24); - - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + // it is possible that the same srcEvent is used with multiple hammer events, + // we keep track on which events are handled in an object _handled + if (!event.srcEvent._handled) { + event.srcEvent._handled = {}; + } - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; + if (event.srcEvent._handled[event.type]) { + return; + } + else { + event.srcEvent._handled[event.type] = true; + } + } - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - data.days = days; - data.months = months; - data.years = years; + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - return this; - } + // propagate over all elements (until stopped) + var elem = _firstTarget; + while (elem && !stopped) { + var _handlers = elem.hammer && elem.hammer._handlers[event.type]; + if (_handlers) { + for (var i = 0; i < _handlers.length && !stopped; i++) { + _handlers[i](event); + } + } - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; + elem = elem.parentNode; + } } - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; - } + return hammer; + }; + })); - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; - units = normalizeUnits(units); +/***/ }, +/* 71 */ +/***/ function(module, exports, __webpack_require__) { - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 24 * 60 + milliseconds / 6e4; - case 'second' : return days * 24 * 60 * 60 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + milliseconds; - default: throw new Error('Unknown unit ' + units); - } - } - } + 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'; - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); - } + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } + var TYPE_FUNCTION = 'function'; - var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); + var round = Math.round; + var abs = Math.abs; + var now = Date.now; - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); - } + /** + * 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); + } - function makeGetter(name) { - return function () { - return this._data[name]; - }; + /** + * 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; + } - var duration_get__milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); + /** + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context + */ + function each(obj, iterator, context) { + var i; - function weeks () { - return absFloor(this.days() / 7); + if (!obj) { + return; } - var round = Math.round; - var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }; + 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++; + } + } else { + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } + } + } - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + /** + * 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; + } - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { - var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); + /** + * 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); + } - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; + /** + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] + */ + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return thresholds[threshold]; - } - thresholds[threshold] = limit; - return true; + if (properties) { + extend(childP, properties); } + } - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); - - if (withSuffix) { - output = locale.pastFuture(+this, output); - } + /** + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} + */ + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - return locale.postformat(output); + /** + * 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; + } - var iso_string__abs = Math.abs; + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } - function iso_string__toISOString() { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = iso_string__abs(this.years()); - var M = iso_string__abs(this.months()); - var D = iso_string__abs(this.days()); - var h = iso_string__abs(this.hours()); - var m = iso_string__abs(this.minutes()); - var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); - var total = this.asSeconds(); + /** + * 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); + }); + } - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + /** + * 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); + }); + } - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); + /** + * 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; + } - var duration_prototype__proto = Duration.prototype; - - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; - duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = duration_get__milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; + /** + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found + */ + function inStr(str, find) { + return str.indexOf(find) > -1; + } - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } - // Side effect imports + /** + * 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; + } + } - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); + /** + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} + */ + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } - // PARSING + /** + * 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; - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); - }); + while (i < src.length) { + var val = key ? src[i][key] : src[i]; + if (inArray(values, val) < 0) { + results.push(src[i]); + } + values[i] = val; + i++; + } - // Side effect imports + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } + } + return results; + } - utils_hooks__hooks.version = '2.10.2'; + /** + * 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); - setHookCallback(local__createLocal); + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; - utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; + if (prop in obj) { + return prop; + } + i++; + } + return undefined; + } - var _moment = utils_hooks__hooks; + /** + * get a unique id + * @returns {number} uniqueId + */ + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } - return _moment; + /** + * 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); + } - })); - /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(86)(module))) + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; -/***/ }, -/* 69 */ -/***/ function(module, exports, __webpack_require__) { + 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 __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - (function (factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - window.propagating = factory(); - } - }(function () { - var _firstTarget = null; // singleton, will contain the target element where the touch event started - var _processing = false; // singleton, true when a touch event is being handled + var COMPUTE_INTERVAL = 25; - /** - * Extend an Hammer.js instance with event propagation. - * - * Features: - * - Events emitted by hammer will propagate in order from child to parent - * elements. - * - Events are extended with a function `event.stopPropagation()` to stop - * propagation to parent elements. - * - An option `preventDefault` to stop all default browser behavior. - * - * Usage: - * var hammer = propagatingHammer(new Hammer(element)); - * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); - * - * @param {Hammer.Manager} hammer An hammer instance. - * @param {Object} [options] Available options: - * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. - * Enforce preventing the default browser behavior. - * Cannot be set to `false`. - * @return {Hammer.Manager} Returns the same hammer instance with extended - * functionality - */ - return function propagating(hammer, options) { - if (options && options.preventDefault === false) { - throw new Error('Only supports preventDefault == true'); - } - var _options = options || { - preventDefault: false - }; + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; - var PropagatingHammer = function(element, options) { - return propagating(new Hammer(element, options), _options); - }; - Hammer.extend(PropagatingHammer, Hammer); - PropagatingHammer.Manager = function (element, options) { - return propagating(new Hammer.Manager(element, options), _options); - }; + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; - return PropagatingHammer; - } + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; - // attach to DOM element - var element = hammer.element; - element.hammer = hammer; + /** + * 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; - // move the original functions that we will wrap - hammer._on = hammer.on; - hammer._off = hammer.off; - hammer._emit = hammer.emit; - hammer._destroy = hammer.destroy; + // 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); + } + }; - /** @type {Object.>} */ - hammer._handlers = {}; + this.init(); - // register an event to catch the start of a gesture and store the - // target in a singleton - hammer._on('hammer.input', function (event) { - if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { - event.preventDefault(); - } - if (event.isFirst) { - _firstTarget = event.target; - _processing = true; - } - if (event.isFinal) { - _processing = false; - } - }); + } + Input.prototype = { /** - * Register a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} handler A callback function, called as handler(event) - * @returns {Hammer.Manager} Returns the hammer instance + * should handle the inputEvent data and trigger the callback + * @virtual */ - hammer.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (!_handlers) { - hammer._handlers[event] = _handlers = []; - - // register the static, propagated handler - hammer._on(event, propagatedHandler); - } - _handlers.push(handler); - }); - - return hammer; - }; + handler: function() { }, /** - * Unregister a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} [handler] Optional. The registered handler. If not - * provided, all handlers for given events - * are removed. - * @returns {Hammer.Manager} Returns the hammer instance + * bind the events */ - hammer.off = function (events, handler) { - // unregister the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (_handlers) { - _handlers = handler ? _handlers.filter(function (h) { - return h !== handler; - }) : []; - - if (_handlers.length > 0) { - hammer._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer._off(event, propagatedHandler); - delete hammer._handlers[event]; - } - } - }); - - return hammer; - }; + 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); + }, /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event + * unbind the events */ - hammer.emit = function(eventType, event) { - if (!_processing) { - _firstTarget = event.target; - } - hammer._emit(eventType, event); - }; + 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); + } + }; - hammer.destroy = function () { - // Detach from DOM element - var element = hammer.element; - delete element.hammer; + /** + * 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; - // clear all handlers - hammer._handlers = {}; + 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); + } + + /** + * 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)); - // call original hammer destroy - hammer._destroy(); - }; + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); + if (isFirst) { + manager.session = {}; } - /** - * A static event handler, applying event propagation. - * @param {Object} event - */ - function propagatedHandler(event) { - // let only a single hammer instance handle this event - if (event.type !== 'hammer.input') { - // it is possible that the same srcEvent is used with multiple hammer events, - // we keep track on which events are handled in an object _handled - if (!event.srcEvent._handled) { - event.srcEvent._handled = {}; - } + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - if (event.srcEvent._handled[event.type]) { - return; - } - else { - event.srcEvent._handled[event.type] = true; - } - } + // compute scale, rotation etc + computeInputData(manager, input); - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; + // emit secret event + manager.emit('hammer.input', input); - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + manager.recognize(input); + manager.session.prevInput = input; + } - // propagate over all elements (until stopped) - var elem = _firstTarget; - while (elem && !stopped) { - var _handlers = elem.hammer && elem.hammer._handlers[event.type]; - if (_handlers) { - for (var i = 0; i < _handlers.length && !stopped; i++) { - _handlers[i](event); - } - } + /** + * 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; - elem = elem.parentNode; - } + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); } - return hammer; - }; - })); + // 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; + } + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; -/***/ }, -/* 70 */ -/***/ function(module, exports, __webpack_require__) { + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - 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'; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - var TYPE_FUNCTION = 'function'; + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + computeIntervalInputData(session, input); - /** - * 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); + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; + } + input.target = target; } - /** - * 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; + 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 + }; } - return false; + + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); } /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input */ - function each(obj, iterator, context) { - var i; + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - if (!obj) { - return; - } + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - 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 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 { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); - } + // 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; } /** - * 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 + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData */ - function extend(dest, src, merge) { - var keys = Object.keys(src); + 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 < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; - } + while (i < input.pointers.length) { + pointers[i] = { + clientX: round(input.pointers[i].clientX), + clientY: round(input.pointers[i].clientY) + }; + i++; + } + + 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) + }; + } + + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; i++; } - return dest; + + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; } /** - * 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 + * 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 merge(dest, src) { - return extend(dest, src, true); + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; } /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; - - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; + } - if (properties) { - extend(childP, properties); + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; } /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} + * 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 bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; + 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]]; + + return Math.sqrt((x * x) + (y * y)); } /** - * 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} + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } - return val; + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; } /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); } /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * 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 addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); } + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; + + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * Mouse events input + * @constructor + * @extends Input */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); + 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); } - /** - * 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; + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; + + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; } - 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; - } + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; + } - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - /** - * 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++; + if (eventType & INPUT_END) { + this.pressed = false; } - return -1; + + 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'; } /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} + * Pointer events input + * @constructor + * @extends Input */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; + + Input.apply(this, arguments); + + this.store = (this.manager.session.pointerEvents = []); } - /** - * 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; + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); - } - values[i] = val; - i++; - } + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); + var isTouch = (pointerType == INPUT_TYPE_TOUCH); + + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + + // 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; } - } - return results; - } + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - /** - * 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); + // update the event in the store + store[storeIndex] = ev; - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - if (prop in obj) { - return prop; + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); } - i++; } - return undefined; - } + }); - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; + + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} + * Touch events input + * @constructor + * @extends Input */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; + + Input.apply(this, arguments); } - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - 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); + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; + if (!this.started) { + return; + } - var COMPUTE_INTERVAL = 25; + var touches = normalizeSingleTouches.call(this, ev, type); - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; + } - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + /** + * @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); - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; + 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 TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} + * Multi-user touch events input * @constructor + * @extends Input */ - 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(); + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; + Input.apply(this, arguments); } - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, - - /** - * bind the events - */ - 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); - }, + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; + } - /** - * 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); + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } - }; + }); /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; + function getTouches(ev, type) { + var allTouches = toArray(ev.touches); + var targetIds = this.targetIds; - 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; + // 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]; } - 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)); + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); + }); - if (isFirst) { - manager.session = {}; + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; + } } - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; + // 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]); + } - // compute scale, rotation etc - computeInputData(manager, input); + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; + } - // emit secret event - manager.emit('hammer.input', input); + if (!changedTargetTouches.length) { + return; + } - manager.recognize(input); - manager.session.prevInput = input; + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; } /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * 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 computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + function TouchMouseInput() { + Input.apply(this, arguments); - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - // 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; - } + 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); - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; + // 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 center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; + } - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); + this.callback(manager, inputEvent, inputData); + }, - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); + } + }); - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - computeIntervalInputData(session, input); + // 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'; - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; - } - input.target = target; + /** + * 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); } - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; + TouchAction.prototype = { + /** + * set the touchAction value on the element or enable the polyfill + * @param {String} value + */ + set: function(value) { + // find out the touch-action by the event handlers + if (value == TOUCH_ACTION_COMPUTE) { + value = this.compute(); + } - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + } + this.actions = value.toLowerCase().trim(); + }, - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); + }, - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + /** + * 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(' ')); + }, - /** - * 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; + /** + * 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 (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; - 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); + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; + } - 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; - } + 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); - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); + } + }, - /** - * 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++; + /** + * 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(); } - - 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 + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} */ - 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) - }; - } - - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; } - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } - - /** - * 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 - }; - } + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - /** - * 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; + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; } - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } - /** - * 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; + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - 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; + return TOUCH_ACTION_AUTO; } /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation + * 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 */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } + 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; /** - * 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 + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } - - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; + function Recognizer(options) { + this.id = uniqueId(); - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + this.manager = null; + this.options = merge(options || {}, this.defaults); - /** - * Mouse events input - * @constructor - * @extends Input - */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state + this.state = STATE_POSSIBLE; - Input.apply(this, arguments); + this.simultaneous = {}; + this.requireFail = []; } - inherit(MouseInput, Input, { + Recognizer.prototype = { /** - * handle mouse events - * @param {Object} ev + * @virtual + * @type {Object} */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; + defaults: {}, - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; } - if (eventType & INPUT_END) { - this.pressed = false; + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); } + return this; + }, - 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 - }; + /** + * 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; + } - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } + /** + * 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; + } - /** - * Pointer events input - * @constructor - * @extends Input - */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - Input.apply(this, arguments); + /** + * 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; + } - this.store = (this.manager.session.pointerEvents = []); - } + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - inherit(PointerEventInput, Input, { /** - * handle mouse events - * @param {Object} ev + * has require failures boolean + * @returns {boolean} */ - 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; + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - var isTouch = (pointerType == INPUT_TYPE_TOUCH); + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + /** + * 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; - // 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; + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); } - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); } - // update the event in the store - store[storeIndex] = ev; - - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + emit(); // simple 'eventName' events - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); } - } - }); - - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; - - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - - /** - * Touch events input - * @constructor - * @extends Input - */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; - - Input.apply(this, arguments); - } + }, - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + /** + * 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; + }, - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; + /** + * 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; + } + i++; } + return true; + }, - if (!this.started) { + /** + * 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; } - var touches = normalizeSingleTouches.call(this, ev, type); - - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; } - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + this.state = this.process(inputDataClone); - /** - * @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); + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } + /** + * 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 - return [all, changed]; - } + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } }; - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - /** - * Multi-user touch events input - * @constructor - * @extends Input + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; - - Input.apply(this, arguments); - } - - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + 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 {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * direction cons to string + * @param {Const} direction + * @returns {String} */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; - - // 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]; - } - - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; - - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); - - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } - - // 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]); - } - - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; - } - i++; + 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 ''; + } - if (!changedTargetTouches.length) { - 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 [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; + return otherRecognizer; } /** - * 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. - * + * This recognizer is just used as a base for the simple attribute recognizers. * @constructor - * @extends Input + * @extends Recognizer */ - 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); + function AttrRecognizer() { + Recognizer.apply(this, arguments); } - inherit(TouchMouseInput, Input, { + inherit(AttrRecognizer, Recognizer, { /** - * handle mouse and touch events - * @param {Hammer} manager - * @param {String} inputEvent - * @param {Object} inputData + * @namespace + * @memberof AttrRecognizer */ - 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; - } - - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, - this.callback(manager, inputEvent, inputData); + /** + * Used to check if it the recognizer receives valid input, like input.distance > 10. + * @memberof AttrRecognizer + * @param {Object} input + * @returns {Boolean} recognized + */ + attrTest: function(input) { + var optionPointers = this.options.pointers; + return optionPointers === 0 || input.pointers.length === optionPointers; }, /** - * remove the event listeners + * Process the input and return the state for the recognizer + * @memberof AttrRecognizer + * @param {Object} input + * @returns {*} State */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); - } - }); + process: function(input) { + var state = this.state; + var eventType = input.eventType; - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - // 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'; + // 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; + } + }); /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value + * Pan + * Recognized when the pointer is down and moved in the allowed direction. * @constructor + * @extends AttrRecognizer */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); + + this.pX = null; + this.pY = null; } - TouchAction.prototype = { + inherit(PanRecognizer, AttrRecognizer, { /** - * set the touchAction value on the element or enable the polyfill - * @param {String} value + * @namespace + * @memberof PanRecognizer */ - set: function(value) { - // find out the touch-action by the event handlers - if (value == TOUCH_ACTION_COMPUTE) { - value = this.compute(); - } + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); } - this.actions = value.toLowerCase().trim(); + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); + } + return actions; }, - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, + 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; - /** - * 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()); + // 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); } - }); - return cleanTouchActions(actions.join(' ')); - }, - - /** - * 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; } + input.direction = direction; + return hasMoved && distance > options.threshold && direction & options.direction; + }, - 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; - } + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - 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); + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); } - }, - /** - * 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(); + this._super.emit.call(this, input); } - }; + }); /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } - - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; - } + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - return TOUCH_ACTION_AUTO; - } + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - /** - * 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 - */ - 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; + 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); + } + } + }); /** - * Recognizer - * Every recognizer needs to extend from this class. + * Press + * Recognized when the pointer is down for x ms without any movement. * @constructor - * @param {Object} options + * @extends Recognizer */ - 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; + function PressRecognizer() { + Recognizer.apply(this, arguments); - this.simultaneous = {}; - this.requireFail = []; + this._timer = null; + this._input = null; } - Recognizer.prototype = { + inherit(PressRecognizer, Recognizer, { /** - * @virtual - * @type {Object} + * @namespace + * @memberof PressRecognizer */ - defaults: {}, + 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 + }, - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; + 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._input = input; + + // 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; }, - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; + reset: function() { + clearTimeout(this._timer); + }, + + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); + 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 this; - }, + } + }); + + /** + * 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, { /** - * drop the simultaneous link. it doesnt remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this + * @namespace + * @memberof RotateRecognizer */ - dropRecognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { - return this; - } + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; }, + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); + + /** + * 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, { /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this + * @namespace + * @memberof SwipeRecognizer */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); }, - /** - * 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; - } + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); + 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; - }, - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; }, + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } + + this.manager.emit(this.options.event, input); + } + }); + + /** + * 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); + + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; + + this._timer = null; + this._input = null; + this.count = 0; + } + + inherit(TapRecognizer, Recognizer, { /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} + * @namespace + * @memberof PinchRecognizer */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; + 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 }, - /** - * 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; - - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } + process: function(input) { + var options = this.options; - emit(); // simple 'eventName' events + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, + this.reset(); - /** - * 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); + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - // it's failing anyway - this.state = STATE_FAILED; - }, - /** - * 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; + // 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(); } - 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); + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } + this.pTime = input.timeStamp; + this.pCenter = input.center; - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } - this.state = this.process(inputDataClone); + this._input = input; - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); + // 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; }, - /** - * 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 - - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } - }; + reset: function() { + clearTimeout(this._timer); + }, - /** - * 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'; + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } } - return ''; - } + }); /** - * direction cons to string - * @param {Const} direction - * @returns {String} + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - 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 ''; + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); } /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} + * @const {string} */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; - } + Hammer.VERSION = '2.0.4'; /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer + * default settings + * @namespace */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } + 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, + + /** + * 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, + + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, + + /** + * 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] + ], - inherit(AttrRecognizer, Recognizer, { /** + * 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 - * @memberof AttrRecognizer */ - defaults: { + cssProps: { /** - * @type {Number} - * @default 1 + * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. + * @type {String} + * @default 'none' */ - pointers: 1 - }, + userSelect: 'none', - /** - * Used to check if it the recognizer receives valid input, like input.distance > 10. - * @memberof AttrRecognizer - * @param {Object} input - * @returns {Boolean} recognized - */ - attrTest: function(input) { - var optionPointers = this.options.pointers; - return optionPointers === 0 || input.pointers.length === optionPointers; - }, + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - /** - * 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; + /** + * 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', - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - // 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; + /** + * 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)' } - }); + }; + + var STOP = 1; + var FORCED_STOP = 2; /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. + * Manager + * @param {HTMLElement} element + * @param {Object} [options] * @constructor - * @extends AttrRecognizer */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + function Manager(element, options) { + options = options || {}; - this.pX = null; - this.pY = null; + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; + + 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); } - inherit(PanRecognizer, AttrRecognizer, { + Manager.prototype = { /** - * @namespace - * @memberof PanRecognizer + * set options + * @param {Object} options + * @returns {Manager} */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + set: function(options) { + extend(this.options, options); - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); } - return actions; + return 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; + /** + * 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; + }, - // 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); - } + /** + * 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; } - input.direction = direction; - return hasMoved && distance > options.threshold && direction & options.direction; - }, - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + var recognizer; + var recognizers = this.recognizers; - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); + // 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; + + // 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; } - this._super.emit.call(this, input); - } - }); + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - /** - * 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); - } + // 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(); + } + + // 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++; + } + }, - inherit(PinchRecognizer, AttrRecognizer, { /** - * @namespace - * @memberof PinchRecognizer + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; + } - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } + } + return null; }, - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + /** + * 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; + } - 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); + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); } - } - }); - /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer - */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + this.recognizers.push(recognizer); + recognizer.manager = this; - this._timer = null; - this._input = null; - } + this.touchAction.update(); + return recognizer; + }, - inherit(PressRecognizer, Recognizer, { /** - * @namespace - * @memberof PressRecognizer + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} */ - 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 - }, - - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - 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 recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - this._input = input; + this.touchAction.update(); + return this; + }, - // 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; + /** + * bind event + * @param {String} events + * @param {Function} handler + * @returns {EventEmitter} this + */ + on: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + handlers[event] = handlers[event] || []; + handlers[event].push(handler); + }); + return this; }, - reset: function() { - clearTimeout(this._timer); + /** + * 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); + } + }); + return this; }, - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { + /** + * emit event to the listeners + * @param {String} event + * @param {Object} data + */ + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); + } + + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { return; } - 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); + 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; } - }); + }; /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); } - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, + /** + * 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); + } - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + 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, - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } + 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 }); - /** - * 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); + if ("function" == TYPE_FUNCTION && __webpack_require__(88)) { + !(__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; } - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, - - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, - - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + })(window, document, 'Hammer'); - 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._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, +/***/ }, +/* 72 */ +/***/ function(module, exports, __webpack_require__) { - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + 'use strict'; - this.manager.emit(this.options.event, input); - } + Object.defineProperty(exports, '__esModule', { + value: true }); - /** - * 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); - - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + 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; }; })(); - this._timer = null; - this._input = null; - this.count = 0; - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - inherit(TapRecognizer, Recognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - 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 - }, + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(48); + var util = __webpack_require__(1); - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + var ColorPicker = (function () { + function ColorPicker() { + var pixelRatio = arguments[0] === undefined ? 1 : arguments[0]; - process: function(input) { - var options = this.options; + _classCallCheck(this, ColorPicker); - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + this.pixelRatio = pixelRatio; + this.generated = false; + this.centerCoordinates = { x: 289 / 2, y: 289 / 2 }; + this.r = 289 * 0.49; + this.color = { r: 255, g: 255, b: 255, a: 1 }; + this.hueCircle = undefined; + this.initialColor = { r: 255, g: 255, b: 255, a: 1 }; + this.previousColor = undefined; + this.applied = false; - this.reset(); + // bound by + this.updateCallback = function () {}; - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } + // create all DOM elements + this._create(); + } - // 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(); - } + _createClass(ColorPicker, [{ + key: 'insertTo', - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + /** + * this inserts the colorPicker into a div from the DOM + * @param container + */ + value: function insertTo(container) { + if (this.hammer !== undefined) { + this.hammer.destroy(); + this.hammer = undefined; + } + this.container = container; + this.container.appendChild(this.frame); + this._bindHammer(); - this.pTime = input.timeStamp; - this.pCenter = input.center; + this._setSize(); + } + }, { + key: 'setCallback', - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + /** + * the callback is executed on apply and save. Bind it to the application + * @param callback + */ + value: function setCallback(callback) { + if (typeof callback === 'function') { + this.updateCallback = callback; + } else { + throw new Error('Function attempted to set as colorPicker callback is not a function.'); + } + } + }, { + key: '_isColorString', + value: function _isColorString(color) { + var htmlColors = { black: '#000000', navy: '#000080', darkblue: '#00008B', mediumblue: '#0000CD', blue: '#0000FF', darkgreen: '#006400', green: '#008000', teal: '#008080', darkcyan: '#008B8B', deepskyblue: '#00BFFF', darkturquoise: '#00CED1', mediumspringgreen: '#00FA9A', lime: '#00FF00', springgreen: '#00FF7F', aqua: '#00FFFF', cyan: '#00FFFF', midnightblue: '#191970', dodgerblue: '#1E90FF', lightseagreen: '#20B2AA', forestgreen: '#228B22', seagreen: '#2E8B57', darkslategray: '#2F4F4F', limegreen: '#32CD32', mediumseagreen: '#3CB371', turquoise: '#40E0D0', royalblue: '#4169E1', steelblue: '#4682B4', darkslateblue: '#483D8B', mediumturquoise: '#48D1CC', indigo: '#4B0082', darkolivegreen: '#556B2F', cadetblue: '#5F9EA0', cornflowerblue: '#6495ED', mediumaquamarine: '#66CDAA', dimgray: '#696969', slateblue: '#6A5ACD', olivedrab: '#6B8E23', slategray: '#708090', lightslategray: '#778899', mediumslateblue: '#7B68EE', lawngreen: '#7CFC00', chartreuse: '#7FFF00', aquamarine: '#7FFFD4', maroon: '#800000', purple: '#800080', olive: '#808000', gray: '#808080', skyblue: '#87CEEB', lightskyblue: '#87CEFA', blueviolet: '#8A2BE2', darkred: '#8B0000', darkmagenta: '#8B008B', saddlebrown: '#8B4513', darkseagreen: '#8FBC8F', lightgreen: '#90EE90', mediumpurple: '#9370D8', darkviolet: '#9400D3', palegreen: '#98FB98', darkorchid: '#9932CC', yellowgreen: '#9ACD32', sienna: '#A0522D', brown: '#A52A2A', darkgray: '#A9A9A9', lightblue: '#ADD8E6', greenyellow: '#ADFF2F', paleturquoise: '#AFEEEE', lightsteelblue: '#B0C4DE', powderblue: '#B0E0E6', firebrick: '#B22222', darkgoldenrod: '#B8860B', mediumorchid: '#BA55D3', rosybrown: '#BC8F8F', darkkhaki: '#BDB76B', silver: '#C0C0C0', mediumvioletred: '#C71585', indianred: '#CD5C5C', peru: '#CD853F', chocolate: '#D2691E', tan: '#D2B48C', lightgrey: '#D3D3D3', palevioletred: '#D87093', thistle: '#D8BFD8', orchid: '#DA70D6', goldenrod: '#DAA520', crimson: '#DC143C', gainsboro: '#DCDCDC', plum: '#DDA0DD', burlywood: '#DEB887', lightcyan: '#E0FFFF', lavender: '#E6E6FA', darksalmon: '#E9967A', violet: '#EE82EE', palegoldenrod: '#EEE8AA', lightcoral: '#F08080', khaki: '#F0E68C', aliceblue: '#F0F8FF', honeydew: '#F0FFF0', azure: '#F0FFFF', sandybrown: '#F4A460', wheat: '#F5DEB3', beige: '#F5F5DC', whitesmoke: '#F5F5F5', mintcream: '#F5FFFA', ghostwhite: '#F8F8FF', salmon: '#FA8072', antiquewhite: '#FAEBD7', linen: '#FAF0E6', lightgoldenrodyellow: '#FAFAD2', oldlace: '#FDF5E6', red: '#FF0000', fuchsia: '#FF00FF', magenta: '#FF00FF', deeppink: '#FF1493', orangered: '#FF4500', tomato: '#FF6347', hotpink: '#FF69B4', coral: '#FF7F50', darkorange: '#FF8C00', lightsalmon: '#FFA07A', orange: '#FFA500', lightpink: '#FFB6C1', pink: '#FFC0CB', gold: '#FFD700', peachpuff: '#FFDAB9', navajowhite: '#FFDEAD', moccasin: '#FFE4B5', bisque: '#FFE4C4', mistyrose: '#FFE4E1', blanchedalmond: '#FFEBCD', papayawhip: '#FFEFD5', lavenderblush: '#FFF0F5', seashell: '#FFF5EE', cornsilk: '#FFF8DC', lemonchiffon: '#FFFACD', floralwhite: '#FFFAF0', snow: '#FFFAFA', yellow: '#FFFF00', lightyellow: '#FFFFE0', ivory: '#FFFFF0', white: '#FFFFFF' }; + if (typeof color === 'string') { + return htmlColors[color]; + } + } + }, { + key: 'setColor', - this._input = input; + /** + * Set the color of the colorPicker + * Supported formats: + * 'red' --> HTML color string + * '#ffffff' --> hex string + * 'rbg(255,255,255)' --> rgb string + * 'rgba(255,255,255,1.0)' --> rgba string + * {r:255,g:255,b:255} --> rgb object + * {r:255,g:255,b:255,a:1.0} --> rgba object + * @param color + * @param setInitial + */ + value: function setColor(color) { + var setInitial = arguments[1] === undefined ? true : arguments[1]; - // 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; - }, + if (color === 'none') { + return; + } - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + var rgba = undefined; - reset: function() { - clearTimeout(this._timer); - }, + // if a html color shorthand is used, convert to hex + var htmlColor = this._isColorString(color); + if (htmlColor !== undefined) { + color = htmlColor; + } - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); + // check format + if (util.isString(color) === true) { + if (util.isValidRGB(color) === true) { + var rgbaArray = color.substr(4).substr(0, color.length - 5).split(','); + rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: 1 }; + } else if (util.isValidRGBA(color) === true) { + var rgbaArray = color.substr(5).substr(0, color.length - 6).split(','); + rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: rgbaArray[3] }; + } else if (util.isValidHex(color) === true) { + var rgbObj = util.hexToRGB(color); + rgba = { r: rgbObj.r, g: rgbObj.g, b: rgbObj.b, a: 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); - } + } else { + if (color instanceof Object) { + if (color.r !== undefined && color.g !== undefined && color.b !== undefined) { + var alpha = color.a !== undefined ? color.a : '1.0'; + rgba = { r: color.r, g: color.g, b: color.b, a: alpha }; + } + } + } - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; + // set color + if (rgba === undefined) { + throw new Error('Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: ' + JSON.stringify(color)); + } else { + this._setColor(rgba, setInitial); + } + } + }, { + key: 'show', - /** - * default settings - * @namespace - */ - 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 + * this shows the color picker at a location. The hue circle is constructed once and stored. + * @param x + * @param y */ - domEvents: false, + value: function show(x, y) { + this.applied = false; + this.frame.style.display = 'block'; + this.frame.style.top = y + 'px'; + this.frame.style.left = x + 'px'; + this._generateHueCircle(); + } + }, { + key: '_hide', - /** - * 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, + // ------------------------------------------ PRIVATE ----------------------------- // /** - * @type {Boolean} - * @default true + * Hide the picker. Is called by the cancel button. + * Optional boolean to store the previous color for easy access later on. + * @param storePrevious + * @private */ - enable: true, + value: function _hide() { + var storePrevious = arguments[0] === undefined ? true : arguments[0]; + + // store the previous color for next time; + if (storePrevious === true) { + this.previousColor = util.extend({}, this.color); + } + + if (this.applied === true) { + this.updateCallback(this.initialColor); + } + + this.frame.style.display = 'none'; + } + }, { + key: '_save', /** - * 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 + * bound to the save button. Saves and hides. + * @private */ - inputTarget: null, + value: function _save() { + this.updateCallback(this.color); + this.applied = false; + this._hide(); + } + }, { + key: '_apply', /** - * force an input class - * @type {Null|Function} - * @default null + * Bound to apply button. Saves but does not close. Is undone by the cancel button. + * @private */ - inputClass: null, + value: function _apply() { + this.applied = true; + this.updateCallback(this.color); + this._updatePicker(this.color); + } + }, { + key: '_loadLast', /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} + * load the color from the previous session. + * @private */ - 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] - ], + value: function _loadLast() { + if (this.previousColor !== undefined) { + this.setColor(this.previousColor, false); + } else { + alert('There is no last color to load...'); + } + } + }, { + key: '_setColor', /** - * 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 + * set the color, place the picker + * @param rgba + * @param setInitial + * @private */ - cssProps: { - /** - * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userSelect: 'none', + value: function _setColor(rgba) { + var setInitial = arguments[1] === undefined ? true : arguments[1]; - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + // store the initial color + if (setInitial === true) { + this.initialColor = util.extend({}, rgba); + } - /** - * 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', + this.color = rgba; + var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + var angleConvert = 2 * Math.PI; + var radius = this.r * hsv.s; + var x = this.centerCoordinates.x + radius * Math.sin(angleConvert * hsv.h); + var y = this.centerCoordinates.y + radius * Math.cos(angleConvert * hsv.h); - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + this.colorPickerSelector.style.left = x - 0.5 * this.colorPickerSelector.clientWidth + 'px'; + this.colorPickerSelector.style.top = y - 0.5 * this.colorPickerSelector.clientHeight + 'px'; - /** - * 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)' + this._updatePicker(rgba); } - }; + }, { + key: '_setOpacity', - var STOP = 1; - var FORCED_STOP = 2; + /** + * bound to opacity control + * @param value + * @private + */ + value: function _setOpacity(value) { + this.color.a = value / 100; + this._updatePicker(this.color); + } + }, { + key: '_setBrightness', - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Manager(element, options) { - options = options || {}; + /** + * bound to brightness control + * @param value + * @private + */ + value: function _setBrightness(value) { + var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); + hsv.v = value / 100; + var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); + rgba['a'] = this.color.a; + this.color = rgba; + this._updatePicker(); + } + }, { + key: '_updatePicker', - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + /** + * update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing. + * @param rgba + * @private + */ + value: function _updatePicker() { + var rgba = arguments[0] === undefined ? this.color : arguments[0]; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); + var ctx = this.colorPickerCanvas.getContext('2d'); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + // clear the canvas + var w = this.colorPickerCanvas.clientWidth; + var h = this.colorPickerCanvas.clientHeight; + ctx.clearRect(0, 0, w, h); - toggleCssProps(this, true); + ctx.putImageData(this.hueCircle, 0, 0); + ctx.fillStyle = 'rgba(0,0,0,' + (1 - hsv.v) + ')'; + ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); + ctx.fill(); - 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.brightnessRange.value = 100 * hsv.v; + this.opacityRange.value = 100 * rgba.a; + + this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; + this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + } + }, { + key: '_setSize', - Manager.prototype = { /** - * set options - * @param {Object} options - * @returns {Manager} + * used by create to set the size of the canvas. + * @private */ - set: function(options) { - extend(this.options, options); + value: function _setSize() { + this.colorPickerCanvas.style.width = '100%'; + this.colorPickerCanvas.style.height = '100%'; - // 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; - }, + this.colorPickerCanvas.width = 289 * this.pixelRatio; + this.colorPickerCanvas.height = 289 * this.pixelRatio; + } + }, { + key: '_create', /** - * 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] + * create all dom elements + * TODO: cleanup, lots of similar dom elements + * @private */ - stop: function(force) { - this.session.stopped = force ? FORCED_STOP : STOP; - }, + value: function _create() { + this.frame = document.createElement('div'); + this.frame.className = 'vis-color-picker'; - /** - * 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; - } + this.colorPickerDiv = document.createElement('div'); + this.colorPickerSelector = document.createElement('div'); + this.colorPickerSelector.className = 'vis-selector'; + this.colorPickerDiv.appendChild(this.colorPickerSelector); - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); + this.colorPickerCanvas = document.createElement('canvas'); + this.colorPickerDiv.appendChild(this.colorPickerCanvas); - var recognizer; - var recognizers = this.recognizers; + if (!this.colorPickerCanvas.getContext) { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.colorPickerCanvas.appendChild(noCanvas); + } else { + var ctx = this.colorPickerCanvas.getContext('2d'); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - // 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; + this.colorPickerCanvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } - // 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; - } + this.colorPickerDiv.className = 'vis-color'; - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + this.opacityDiv = document.createElement('div'); + this.opacityDiv.className = 'vis-opacity'; - // 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.brightnessDiv = document.createElement('div'); + this.brightnessDiv.className = 'vis-brightness'; - // 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.arrowDiv = document.createElement('div'); + this.arrowDiv.className = 'vis-arrow'; - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } + this.opacityRange = document.createElement('input'); + this.opacityRange.type = 'range'; + this.opacityRange.min = '0'; + this.opacityRange.max = '100'; + this.opacityRange.value = '100'; + this.opacityRange.className = 'vis-range'; - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; - }, + this.brightnessRange = document.createElement('input'); + this.brightnessRange.type = 'range'; + this.brightnessRange.min = '0'; + this.brightnessRange.max = '100'; + this.brightnessRange.value = '100'; + this.brightnessRange.className = 'vis-range'; - /** - * 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; - } + this.opacityDiv.appendChild(this.opacityRange); + this.brightnessDiv.appendChild(this.brightnessRange); - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } + var me = this; + this.opacityRange.onchange = function () { + me._setOpacity(this.value); + }; + this.opacityRange.oninput = function () { + me._setOpacity(this.value); + }; + this.brightnessRange.onchange = function () { + me._setBrightness(this.value); + }; + this.brightnessRange.oninput = function () { + me._setBrightness(this.value); + }; - this.recognizers.push(recognizer); - recognizer.manager = this; + this.brightnessLabel = document.createElement('div'); + this.brightnessLabel.className = 'vis-label vis-brightness'; + this.brightnessLabel.innerHTML = 'brightness:'; - this.touchAction.update(); - return recognizer; - }, + this.opacityLabel = document.createElement('div'); + this.opacityLabel.className = 'vis-label vis-opacity'; + this.opacityLabel.innerHTML = 'opacity:'; - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } + this.newColorDiv = document.createElement('div'); + this.newColorDiv.className = 'vis-new-color'; + this.newColorDiv.innerHTML = 'new'; + + this.initialColorDiv = document.createElement('div'); + this.initialColorDiv.className = 'vis-initial-color'; + this.initialColorDiv.innerHTML = 'initial'; + + this.cancelButton = document.createElement('div'); + this.cancelButton.className = 'vis-button vis-cancel'; + this.cancelButton.innerHTML = 'cancel'; + this.cancelButton.onclick = this._hide.bind(this, false); + + this.applyButton = document.createElement('div'); + this.applyButton.className = 'vis-button vis-apply'; + this.applyButton.innerHTML = 'apply'; + this.applyButton.onclick = this._apply.bind(this); - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); + this.saveButton = document.createElement('div'); + this.saveButton.className = 'vis-button vis-save'; + this.saveButton.innerHTML = 'save'; + this.saveButton.onclick = this._save.bind(this); - this.touchAction.update(); - return this; - }, + this.loadButton = document.createElement('div'); + this.loadButton.className = 'vis-button vis-load'; + this.loadButton.innerHTML = 'load last'; + this.loadButton.onclick = this._loadLast.bind(this); - /** - * bind event - * @param {String} events - * @param {Function} handler - * @returns {EventEmitter} this - */ - on: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - handlers[event] = handlers[event] || []; - handlers[event].push(handler); - }); - return this; - }, + this.frame.appendChild(this.colorPickerDiv); + this.frame.appendChild(this.arrowDiv); + this.frame.appendChild(this.brightnessLabel); + this.frame.appendChild(this.brightnessDiv); + this.frame.appendChild(this.opacityLabel); + this.frame.appendChild(this.opacityDiv); + this.frame.appendChild(this.newColorDiv); + this.frame.appendChild(this.initialColorDiv); - /** - * 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); - } - }); - return this; - }, + this.frame.appendChild(this.cancelButton); + this.frame.appendChild(this.applyButton); + this.frame.appendChild(this.saveButton); + this.frame.appendChild(this.loadButton); + } + }, { + key: '_bindHammer', /** - * emit event to the listeners - * @param {String} event - * @param {Object} data + * bind hammer to the color picker + * @private */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); - } - - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; - } + value: function _bindHammer() { + var _this = this; - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + this.drag = {}; + this.pinch = {}; + this.hammer = new Hammer(this.colorPickerCanvas); + this.hammer.get('pinch').set({ enable: true }); - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; - } - }, + hammerUtil.onTouch(this.hammer, function (event) { + _this._moveSelector(event); + }); + this.hammer.on('tap', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panstart', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panmove', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panend', function (event) { + _this._moveSelector(event); + }); + } + }, { + key: '_generateHueCircle', /** - * destroy the manager and unbinds all events - * it doesn't unbind dom events, that is the user own responsibility + * generate the hue circle. This is relatively heavy (200ms) and is done only once on the first time it is shown. + * @private */ - destroy: function() { - this.element && toggleCssProps(this, false); + value: function _generateHueCircle() { + if (this.generated === false) { + var ctx = this.colorPickerCanvas.getContext('2d'); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; - } - }; + // clear the canvas + var w = this.colorPickerCanvas.clientWidth; + var h = this.colorPickerCanvas.clientHeight; + ctx.clearRect(0, 0, w, h); - /** - * 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 : ''; - }); - } + // draw hue circle + var x = undefined, + y = undefined, + hue = undefined, + sat = undefined; + this.centerCoordinates = { x: w * 0.5, y: h * 0.5 }; + this.r = 0.49 * w; + var angleConvert = 2 * Math.PI / 360; + var hfac = 1 / 360; + var sfac = 1 / this.r; + var rgb = undefined; + for (hue = 0; hue < 360; hue++) { + for (sat = 0; sat < this.r; sat++) { + x = this.centerCoordinates.x + sat * Math.sin(angleConvert * hue); + y = this.centerCoordinates.y + sat * Math.cos(angleConvert * hue); + rgb = util.HSVToRGB(hue * hfac, sat * sfac, 1); + ctx.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; + ctx.fillRect(x - 0.5, y - 0.5, 2, 2); + } + } + ctx.strokeStyle = 'rgba(0,0,0,1)'; + ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); + ctx.stroke(); - /** - * 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); - } + this.hueCircle = ctx.getImageData(0, 0, w, h); + } + this.generated = true; + } + }, { + key: '_moveSelector', - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, + /** + * move the selector. This is called by hammer functions. + * + * @param event + * @private + */ + value: function _moveSelector(event) { + var rect = this.colorPickerDiv.getBoundingClientRect(); + var left = event.center.x - rect.left; + var top = event.center.y - rect.top; - 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, + var centerY = 0.5 * this.colorPickerDiv.clientHeight; + var centerX = 0.5 * this.colorPickerDiv.clientWidth; - 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, + var x = left - centerX; + var y = top - centerY; - Manager: Manager, - Input: Input, - TouchAction: TouchAction, + var angle = Math.atan2(x, y); + var radius = 0.98 * Math.min(Math.sqrt(x * x + y * y), centerX); - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, + var newTop = Math.cos(angle) * radius + centerY; + var newLeft = Math.sin(angle) * radius + centerX; - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, + this.colorPickerSelector.style.top = newTop - 0.5 * this.colorPickerSelector.clientHeight + 'px'; + this.colorPickerSelector.style.left = newLeft - 0.5 * this.colorPickerSelector.clientWidth + 'px'; - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); + // set color + var h = angle / (2 * Math.PI); + h = h < 0 ? h + 1 : h; + var s = radius / this.r; + var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); + hsv.h = h; + hsv.s = s; + var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); + rgba['a'] = this.color.a; + this.color = rgba; - if ("function" == TYPE_FUNCTION && __webpack_require__(87)) { - !(__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; - } + // update previews + this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; + this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + } + }]); - })(window, document, 'Hammer'); + return ColorPicker; + })(); + exports['default'] = ColorPicker; + module.exports = exports['default']; /***/ }, -/* 71 */ +/* 73 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -33275,67 +34116,67 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _sharedLabel = __webpack_require__(72); + var _sharedLabel = __webpack_require__(74); var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - var _nodesShapesBox = __webpack_require__(88); + var _nodesShapesBox = __webpack_require__(89); var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); - var _nodesShapesCircle = __webpack_require__(89); + var _nodesShapesCircle = __webpack_require__(90); var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); - var _nodesShapesCircularImage = __webpack_require__(90); + var _nodesShapesCircularImage = __webpack_require__(91); var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); - var _nodesShapesDatabase = __webpack_require__(91); + var _nodesShapesDatabase = __webpack_require__(92); var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); - var _nodesShapesDiamond = __webpack_require__(92); + var _nodesShapesDiamond = __webpack_require__(93); var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); - var _nodesShapesDot = __webpack_require__(93); + var _nodesShapesDot = __webpack_require__(94); var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); - var _nodesShapesEllipse = __webpack_require__(94); + var _nodesShapesEllipse = __webpack_require__(95); var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); - var _nodesShapesIcon = __webpack_require__(95); + var _nodesShapesIcon = __webpack_require__(96); var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); - var _nodesShapesImage = __webpack_require__(96); + var _nodesShapesImage = __webpack_require__(97); var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); - var _nodesShapesSquare = __webpack_require__(97); + var _nodesShapesSquare = __webpack_require__(98); var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); - var _nodesShapesStar = __webpack_require__(98); + var _nodesShapesStar = __webpack_require__(99); var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); - var _nodesShapesText = __webpack_require__(99); + var _nodesShapesText = __webpack_require__(100); var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); - var _nodesShapesTriangle = __webpack_require__(100); + var _nodesShapesTriangle = __webpack_require__(101); var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); - var _nodesShapesTriangleDown = __webpack_require__(101); + var _nodesShapesTriangleDown = __webpack_require__(102); var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); - var _Validator = __webpack_require__(63); + var _Validator = __webpack_require__(46); var _Validator2 = _interopRequireDefault(_Validator); @@ -33571,506 +34412,191 @@ return /******/ (function(modules) { // webpackBootstrap * unselect this node */ value: function unselect() { - this.selected = false; - this._reset(); - } - }, { - key: '_reset', - - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - value: function _reset() { - this.shape.width = undefined; - this.shape.height = undefined; - } - }, { - key: 'getTitle', - - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - value: function getTitle() { - return this.options.title; - } - }, { - key: 'distanceToBorder', - - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - value: function distanceToBorder(ctx, angle) { - return this.shape.distanceToBorder(ctx, angle); - } - }, { - key: 'isFixed', - - /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not - */ - value: function isFixed() { - return this.options.fixed.x && this.options.fixed.y; - } - }, { - key: 'isSelected', - - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', - - /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', - - /** - * Adjust the value range of the node. The node will adjust it's size - * based on its value. - * @param {Number} min - * @param {Number} max - */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var sizeDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.size = this.options.scaling.min + scale * sizeDiff; - } else { - this.options.size = this.baseSize; - this.options.font.size = this.baseFontSize; - } - } - }, { - key: 'draw', - - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); - } - }, { - key: 'resize', - - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function resize(ctx) { - this.shape.resize(ctx); - } - }, { - key: 'isOverlappingWith', - - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isOverlappingWith(obj) { - return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; - } - }, { - key: 'isBoundingBoxOverlappingWith', - - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isBoundingBoxOverlappingWith(obj) { - return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; - } - }], [{ - key: 'parseOptions', - - /** - * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. - * Static so it can also be used by the handler. - * @param parentOptions - * @param newOptions - */ - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - - var fields = ['color', 'fixed', 'shadow']; - util.selectiveNotDeepExtend(fields, parentOptions, newOptions); - - // merge the shadow options into the parent. - util.mergeOptions(parentOptions, newOptions, 'shadow'); - - // individual shape newOptions - if (newOptions.color !== undefined && newOptions.color !== null) { - var parsedColor = util.parseColor(newOptions.color); - util.fillIfDefined(parentOptions.color, parsedColor); - } else if (allowDeletion === true) { - parentOptions.color = undefined; - delete parentOptions.color; - } - - if (newOptions.fixed !== undefined && newOptions.fixed !== null) { - if (typeof newOptions.fixed === 'boolean') { - parentOptions.fixed.x = newOptions.fixed; - parentOptions.fixed.y = newOptions.fixed; - } else { - if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { - parentOptions.fixed.x = newOptions.fixed.x; - } - if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { - parentOptions.fixed.y = newOptions.fixed.y; - } - } - } - } - }]); - - return Node; - })(); - - exports['default'] = Node; - module.exports = exports['default']; - -/***/ }, -/* 72 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(1); - - var Label = (function () { - function Label(body, options) { - _classCallCheck(this, Label); - - this.body = body; - - this.baseSize = undefined; - this.setOptions(options); - this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached - } - - _createClass(Label, [{ - key: 'setOptions', - value: function setOptions(options) { - var allowDeletion = arguments[1] === undefined ? false : arguments[1]; - - this.options = options; - - if (options.label !== undefined) { - this.labelDirty = true; - } - - if (options.font !== undefined) { - Label.parseOptions(this.options.font, options, allowDeletion); - if (typeof options.font === 'string') { - this.baseSize = this.options.font.size; - } else if (typeof options.font === 'object') { - if (options.font.size !== undefined) { - this.baseSize = options.font.size; - } - } - } + this.selected = false; + this._reset(); } }, { - key: 'draw', + key: '_reset', /** - * Main function. This is called from anything that wants to draw a label. - * @param ctx - * @param x - * @param y - * @param selected - * @param baseline + * Reset the calculated size of the node, forces it to recalculate its size + * @private */ - value: function draw(ctx, x, y, selected) { - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - - // if no label, return - if (this.options.label === undefined) return; - - // check if we have to render the label - var viewFontSize = this.options.font.size * this.body.view.scale; - if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; - - // update the size cache if required - this.calculateLabelSize(ctx, selected, x, y, baseline); - - // create the fontfill background - this._drawBackground(ctx); - // draw text - this._drawText(ctx, selected, x, y, baseline); + value: function _reset() { + this.shape.width = undefined; + this.shape.height = undefined; } }, { - key: '_drawBackground', + key: 'getTitle', /** - * Draws the label background - * @param {CanvasRenderingContext2D} ctx - * @private + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. */ - value: function _drawBackground(ctx) { - if (this.options.font.background !== undefined && this.options.font.background !== 'none') { - ctx.fillStyle = this.options.font.background; - - var lineMargin = 2; - - switch (this.options.font.align) { - case 'middle': - ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); - break; - case 'top': - ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); - break; - case 'bottom': - ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); - break; - default: - ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); - break; - } - } + value: function getTitle() { + return this.options.title; } }, { - key: '_drawText', + key: 'distanceToBorder', /** - * - * @param ctx - * @param x - * @param baseline - * @private + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels */ - value: function _drawText(ctx, selected, x, y) { - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - - var fontSize = this.options.font.size; - var viewFontSize = fontSize * this.body.view.scale; - // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (viewFontSize >= this.options.scaling.label.maxVisible) { - fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; - } - - var yLine = this.size.yLine; - - var _getColor = this._getColor(viewFontSize); - - var _getColor2 = _slicedToArray(_getColor, 2); - - var fontColor = _getColor2[0]; - var strokeColor = _getColor2[1]; - - var _setAlignment = this._setAlignment(ctx, x, yLine, baseline); - - var _setAlignment2 = _slicedToArray(_setAlignment, 2); - - x = _setAlignment2[0]; - yLine = _setAlignment2[1]; + value: function distanceToBorder(ctx, angle) { + return this.shape.distanceToBorder(ctx, angle); + } + }, { + key: 'isFixed', - // configure context for drawing the text - ctx.font = (selected ? 'bold ' : '') + fontSize + 'px ' + this.options.font.face; - ctx.fillStyle = fontColor; - ctx.textAlign = 'center'; + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + value: function isFixed() { + return this.options.fixed.x && this.options.fixed.y; + } + }, { + key: 'isSelected', - // set the strokeWidth - if (this.options.font.strokeWidth > 0) { - ctx.lineWidth = this.options.font.strokeWidth; - ctx.strokeStyle = strokeColor; - ctx.lineJoin = 'round'; - } + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + } + }, { + key: 'getValue', - // draw the text - for (var i = 0; i < this.lineCount; i++) { - if (this.options.font.strokeWidth > 0) { - ctx.strokeText(this.lines[i], x, yLine); - } - ctx.fillText(this.lines[i], x, yLine); - yLine += fontSize; - } + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; } }, { - key: '_setAlignment', - value: function _setAlignment(ctx, x, yLine, baseline) { - // check for label alignment (for edges) - // TODO: make alignment for nodes - if (this.options.font.align !== 'horizontal') { - x = 0; - yLine = 0; + key: 'setValueRange', - var lineMargin = 2; - if (this.options.font.align === 'top') { - ctx.textBaseline = 'alphabetic'; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.options.font.align === 'bottom') { - ctx.textBaseline = 'hanging'; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers - } else { - ctx.textBaseline = 'middle'; + /** + * Adjust the value range of the node. The node will adjust it's size + * based on its value. + * @param {Number} min + * @param {Number} max + */ + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var sizeDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } + this.options.size = this.options.scaling.min + scale * sizeDiff; } else { - ctx.textBaseline = baseline; + this.options.size = this.baseSize; + this.options.font.size = this.baseFontSize; } - - return [x, yLine]; } }, { - key: '_getColor', + key: 'draw', /** - * fade in when relative scale is between threshold and threshold - 1. - * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. - * - * @param viewFontSize - * @returns {*[]} - * @private + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - value: function _getColor(viewFontSize) { - var fontColor = this.options.font.color || '#000000'; - var strokeColor = this.options.font.strokeColor || '#ffffff'; - if (viewFontSize <= this.options.scaling.label.drawThreshold) { - var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokeColor = util.overrideOpacity(strokeColor, opacity); - } - return [fontColor, strokeColor]; + value: function draw(ctx) { + this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); } }, { - key: 'getTextSize', + key: 'resize', /** - * - * @param ctx - * @param selected - * @returns {{width: number, height: number}} + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - value: function getTextSize(ctx) { - var selected = arguments[1] === undefined ? false : arguments[1]; - - var size = { - width: this._processLabel(ctx, selected), - height: this.options.font.size * this.lineCount, - lineCount: this.lineCount - }; - return size; + value: function resize(ctx) { + this.shape.resize(ctx); } }, { - key: 'calculateLabelSize', + key: 'isOverlappingWith', /** - * - * @param ctx - * @param selected - * @param x - * @param y - * @param baseline + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node */ - value: function calculateLabelSize(ctx, selected) { - var x = arguments[2] === undefined ? 0 : arguments[2]; - var y = arguments[3] === undefined ? 0 : arguments[3]; - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - - if (this.labelDirty === true) { - this.size.width = this._processLabel(ctx, selected); - } - this.size.height = this.options.font.size * this.lineCount; - this.size.left = x - this.size.width * 0.5; - this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; - if (baseline === 'hanging') { - this.size.top += 0.5 * this.options.font.size; - this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - this.size.yLine += 4; // distance from node - } - - this.labelDirty = false; + value: function isOverlappingWith(obj) { + return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; } }, { - key: '_processLabel', + key: 'isBoundingBoxOverlappingWith', /** - * This calculates the width as well as explodes the label string and calculates the amount of lines. - * @param ctx - * @param selected - * @returns {number} - * @private + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node */ - value: function _processLabel(ctx, selected) { - var width = 0; - var lines = ['']; - var lineCount = 0; - if (this.options.label !== undefined) { - lines = String(this.options.label).split('\n'); - lineCount = lines.length; - ctx.font = (selected ? 'bold ' : '') + this.options.font.size + 'px ' + this.options.font.face; - width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } - } - this.lines = lines; - this.lineCount = lineCount; - - return width; + value: function isBoundingBoxOverlappingWith(obj) { + return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; } }], [{ key: 'parseOptions', + + /** + * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. + * Static so it can also be used by the handler. + * @param parentOptions + * @param newOptions + */ value: function parseOptions(parentOptions, newOptions) { var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - if (typeof newOptions.font === 'string') { - var newOptionsArray = newOptions.font.split(' '); - parentOptions.size = newOptionsArray[0].replace('px', ''); - parentOptions.face = newOptionsArray[1]; - parentOptions.color = newOptionsArray[2]; - } else if (typeof newOptions.font === 'object') { - util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); + var fields = ['color', 'fixed', 'shadow']; + util.selectiveNotDeepExtend(fields, parentOptions, newOptions); + + // merge the shadow options into the parent. + util.mergeOptions(parentOptions, newOptions, 'shadow'); + + // individual shape newOptions + if (newOptions.color !== undefined && newOptions.color !== null) { + var parsedColor = util.parseColor(newOptions.color); + util.fillIfDefined(parentOptions.color, parsedColor); + } else if (allowDeletion === true) { + parentOptions.color = undefined; + delete parentOptions.color; + } + + if (newOptions.fixed !== undefined && newOptions.fixed !== null) { + if (typeof newOptions.fixed === 'boolean') { + parentOptions.fixed.x = newOptions.fixed; + parentOptions.fixed.y = newOptions.fixed; + } else { + if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { + parentOptions.fixed.x = newOptions.fixed.x; + } + if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { + parentOptions.fixed.y = newOptions.fixed.y; + } + } } - parentOptions.size = Number(parentOptions.size); } }]); - return Label; + return Node; })(); - exports['default'] = Label; + exports['default'] = Node; module.exports = exports['default']; /***/ }, -/* 73 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -34081,1027 +34607,861 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _sharedLabel = __webpack_require__(72); - - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - - var _edgesBezierEdgeDynamic = __webpack_require__(102); - - var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); - - var _edgesBezierEdgeStatic = __webpack_require__(103); - - var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); - - var _edgesStraightEdge = __webpack_require__(104); - - var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); - var util = __webpack_require__(1); - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ - - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + var Label = (function () { + function Label(body, options) { + _classCallCheck(this, Label); - if (body === undefined) { - throw 'No body provided'; - } - this.options = util.bridgeObject(globalOptions); this.body = body; - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; - - this.baseWidth = this.options.width; - this.baseFontSize = this.options.font.size; - - this.from = undefined; // a node - this.to = undefined; // a node - - this.edgeType = undefined; - - this.connected = false; - - this.labelModule = new _sharedLabel2['default'](this.body, this.options); - + this.baseSize = undefined; this.setOptions(options); + this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached } - _createClass(Edge, [{ + _createClass(Label, [{ key: 'setOptions', - - /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ value: function setOptions(options) { - if (!options) { - return; - } - this.colorDirty = true; + var allowDeletion = arguments[1] === undefined ? false : arguments[1]; - Edge.parseOptions(this.options, options, true); + this.options = options; - if (options.id !== undefined) { - this.id = options.id; - } - if (options.from !== undefined) { - this.fromId = options.from; - } - if (options.to !== undefined) { - this.toId = options.to; - } - if (options.title !== undefined) { - this.title = options.title; - } - if (options.value !== undefined) { - options.value = parseInt(options.value); + if (options.label !== undefined) { + this.labelDirty = true; } - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); - - // update label Module - this.updateLabelModule(); - - var dataChanged = this.updateEdgeType(); - - // if anything has been updates, reset the selection width and the hover width - this._setInteractionWidths(); - - return dataChanged; - } - }, { - key: 'updateLabelModule', - - /** - * update the options in the label module - */ - value: function updateLabelModule() { - this.labelModule.setOptions(this.options, true); - if (this.labelModule.baseSize !== undefined) { - this.baseFontSize = this.labelModule.baseSize; + if (options.font !== undefined) { + Label.parseOptions(this.options.font, options, allowDeletion); + if (typeof options.font === 'string') { + this.baseSize = this.options.font.size; + } else if (typeof options.font === 'object') { + if (options.font.size !== undefined) { + this.baseSize = options.font.size; + } + } } } }, { - key: 'updateEdgeType', + key: 'draw', /** - * update the edge type, set the options - * @returns {boolean} + * Main function. This is called from anything that wants to draw a label. + * @param ctx + * @param x + * @param y + * @param selected + * @param baseline */ - value: function updateEdgeType() { - var dataChanged = false; - var changeInType = true; - if (this.edgeType !== undefined) { - if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.dynamic === true) { - changeInType = false; - } - if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.dynamic === false) { - changeInType = false; - } - if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { - changeInType = false; - } - - if (changeInType === true) { - dataChanged = this.edgeType.cleanup(); - } - } + value: function draw(ctx, x, y, selected) { + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - if (changeInType === true) { - if (this.options.smooth.enabled === true) { - if (this.options.smooth.dynamic === true) { - dataChanged = true; - this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); - } else { - this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); - } - } else { - this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); - } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); - } + // if no label, return + if (this.options.label === undefined) return; - return dataChanged; - } - }, { - key: 'togglePhysics', + // check if we have to render the label + var viewFontSize = this.options.font.size * this.body.view.scale; + if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - this.options.physics = status; - this.edgeType.togglePhysics(status); + // update the size cache if required + this.calculateLabelSize(ctx, selected, x, y, baseline); + + // create the fontfill background + this._drawBackground(ctx); + // draw text + this._drawText(ctx, selected, x, y, baseline); } }, { - key: 'connect', + key: '_drawBackground', /** - * Connect an edge to its nodes + * Draws the label background + * @param {CanvasRenderingContext2D} ctx + * @private */ - value: function connect() { - this.disconnect(); + value: function _drawBackground(ctx) { + if (this.options.font.background !== undefined && this.options.font.background !== 'none') { + ctx.fillStyle = this.options.font.background; - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + var lineMargin = 2; - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); + switch (this.options.font.align) { + case 'middle': + ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); + break; + case 'top': + ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); + break; + case 'bottom': + ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); + break; + default: + ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); + break; } } } }, { - key: 'disconnect', + key: '_drawText', /** - * Disconnect an edge from its nodes + * + * @param ctx + * @param x + * @param baseline + * @private */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; - } - if (this.to) { - this.to.detachEdge(this); - this.to = undefined; + value: function _drawText(ctx, selected, x, y) { + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; + + var fontSize = this.options.font.size; + var viewFontSize = fontSize * this.body.view.scale; + // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) + if (viewFontSize >= this.options.scaling.label.maxVisible) { + fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; } - this.connected = false; - } - }, { - key: 'getTitle', + var yLine = this.size.yLine; - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - value: function getTitle() { - return this.title; - } - }, { - key: 'isSelected', + var _getColor = this._getColor(viewFontSize); - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', + var _getColor2 = _slicedToArray(_getColor, 2); - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', + var fontColor = _getColor2[0]; + var strokeColor = _getColor2[1]; - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total - */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.width = this.options.scaling.min + scale * widthDiff; - } else { - this.options.width = this.baseWidth; - this.options.font.size = this.baseFontSize; + var _setAlignment = this._setAlignment(ctx, x, yLine, baseline); + + var _setAlignment2 = _slicedToArray(_setAlignment, 2); + + x = _setAlignment2[0]; + yLine = _setAlignment2[1]; + + // configure context for drawing the text + ctx.font = (selected ? 'bold ' : '') + fontSize + 'px ' + this.options.font.face; + ctx.fillStyle = fontColor; + ctx.textAlign = 'center'; + + // set the strokeWidth + if (this.options.font.strokeWidth > 0) { + ctx.lineWidth = this.options.font.strokeWidth; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = 'round'; } - this._setInteractionWidths(); + // draw the text + for (var i = 0; i < this.lineCount; i++) { + if (this.options.font.strokeWidth > 0) { + ctx.strokeText(this.lines[i], x, yLine); + } + ctx.fillText(this.lines[i], x, yLine); + yLine += fontSize; + } } }, { - key: '_setInteractionWidths', - value: function _setInteractionWidths() { - if (typeof this.options.hoverWidth === 'function') { - this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); - } else { - this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; - } + key: '_setAlignment', + value: function _setAlignment(ctx, x, yLine, baseline) { + // check for label alignment (for edges) + // TODO: make alignment for nodes + if (this.options.font.align !== 'horizontal') { + x = 0; + yLine = 0; - if (typeof this.options.selectionWidth === 'function') { - this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); + var lineMargin = 2; + if (this.options.font.align === 'top') { + ctx.textBaseline = 'alphabetic'; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.options.font.align === 'bottom') { + ctx.textBaseline = 'hanging'; + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { + ctx.textBaseline = 'middle'; + } } else { - this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; + ctx.textBaseline = baseline; } + + return [x, yLine]; } }, { - key: 'draw', + key: '_getColor', /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * fade in when relative scale is between threshold and threshold - 1. + * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. + * + * @param viewFontSize + * @returns {*[]} + * @private */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); - } - }, { - key: 'drawArrows', - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); - } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); - } - } - }, { - key: 'drawLabel', - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); - - // if the label has to be rotated: - if (this.options.font.align !== 'horizontal') { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); - } - - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.height * 0.5; - } - point = this._pointOnCircle(x, y, radius, 0.125); - - this.labelModule.draw(ctx, point.x, point.y, selected); - } + value: function _getColor(viewFontSize) { + var fontColor = this.options.font.color || '#000000'; + var strokeColor = this.options.font.strokeColor || '#ffffff'; + if (viewFontSize <= this.options.scaling.label.drawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokeColor = util.overrideOpacity(strokeColor, opacity); } + return [fontColor, strokeColor]; } }, { - key: 'isOverlappingWith', + key: 'getTextSize', /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge + * + * @param ctx + * @param selected + * @returns {{width: number, height: number}} */ - value: function isOverlappingWith(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + value: function getTextSize(ctx) { + var selected = arguments[1] === undefined ? false : arguments[1]; - return dist < distMax; - } else { - return false; - } + var size = { + width: this._processLabel(ctx, selected), + height: this.options.font.size * this.lineCount, + lineCount: this.lineCount + }; + return size; } }, { - key: '_rotateForLabelAlignment', + key: 'calculateLabelSize', /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private + * + * @param ctx + * @param selected + * @param x + * @param y + * @param baseline */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + value: function calculateLabelSize(ctx, selected) { + var x = arguments[2] === undefined ? 0 : arguments[2]; + var y = arguments[3] === undefined ? 0 : arguments[3]; + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + if (this.labelDirty === true) { + this.size.width = this._processLabel(ctx, selected); + } + this.size.height = this.options.font.size * this.lineCount; + this.size.left = x - this.size.width * 0.5; + this.size.top = y - this.size.height * 0.5; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; + if (baseline === 'hanging') { + this.size.top += 0.5 * this.options.font.size; + this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + this.size.yLine += 4; // distance from node } - ctx.rotate(angleInDegrees); + this.labelDirty = false; } }, { - key: '_pointOnCircle', + key: '_processLabel', /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point + * This calculates the width as well as explodes the label string and calculates the amount of lines. + * @param ctx + * @param selected + * @returns {number} * @private */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - } - }, { - key: 'select', - value: function select() { - this.selected = true; - } - }, { - key: 'unselect', - value: function unselect() { - this.selected = false; + value: function _processLabel(ctx, selected) { + var width = 0; + var lines = ['']; + var lineCount = 0; + if (this.options.label !== undefined) { + lines = String(this.options.label).split('\n'); + lineCount = lines.length; + ctx.font = (selected ? 'bold ' : '') + this.options.font.size + 'px ' + this.options.font.face; + width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; + } + } + this.lines = lines; + this.lineCount = lineCount; + + return width; } }], [{ key: 'parseOptions', value: function parseOptions(parentOptions, newOptions) { var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - var fields = ['id', 'font', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; + if (typeof newOptions.font === 'string') { + var newOptionsArray = newOptions.font.split(' '); + parentOptions.size = newOptionsArray[0].replace('px', ''); + parentOptions.face = newOptionsArray[1]; + parentOptions.color = newOptionsArray[2]; + } else if (typeof newOptions.font === 'object') { + util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); + } + parentOptions.size = Number(parentOptions.size); + } + }]); - // only deep extend the items in the field array. These do not have shorthand. - util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); + return Label; + })(); - util.mergeOptions(parentOptions, newOptions, 'smooth'); - util.mergeOptions(parentOptions, newOptions, 'shadow'); + exports['default'] = Label; + module.exports = exports['default']; - if (newOptions.dashes !== undefined && newOptions.dashes !== null) { - parentOptions.dashes = newOptions.dashes; - } else if (allowDeletion === true) { - parentOptions.dashes = undefined; - delete parentOptions.dashes; - } +/***/ }, +/* 75 */ +/***/ function(module, exports, __webpack_require__) { - // set the scaling newOptions - if (newOptions.scaling !== undefined && newOptions.scaling !== null) { - if (newOptions.scaling.min !== undefined) { - parentOptions.scaling.min = newOptions.scaling.min; - } - if (newOptions.scaling.max !== undefined) { - parentOptions.scaling.max = newOptions.scaling.max; - } - util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); - } else if (allowDeletion === true) { - parentOptions.scaling = undefined; - delete parentOptions.scaling; - } + 'use strict'; - // hanlde multiple input cases for arrows - if (newOptions.arrows !== undefined && newOptions.arrows !== null) { - if (typeof newOptions.arrows === 'string') { - var arrows = newOptions.arrows.toLowerCase(); - if (arrows.indexOf('to') != -1) { - parentOptions.arrows.to.enabled = true; - } - if (arrows.indexOf('middle') != -1) { - parentOptions.arrows.middle.enabled = true; - } - if (arrows.indexOf('from') != -1) { - parentOptions.arrows.from.enabled = true; - } - } else if (typeof newOptions.arrows === 'object') { - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); - } else { - throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); - } - } else if (allowDeletion === true) { - parentOptions.arrows = undefined; - delete parentOptions.arrows; - } + Object.defineProperty(exports, '__esModule', { + value: true + }); - // hanlde multiple input cases for color - if (newOptions.color !== undefined && newOptions.color !== null) { - if (util.isString(newOptions.color)) { - parentOptions.color.color = newOptions.color; - parentOptions.color.highlight = newOptions.color; - parentOptions.color.hover = newOptions.color; - parentOptions.color.inherit = false; - } else { - var colorsDefined = false; - if (newOptions.color.color !== undefined) { - parentOptions.color.color = newOptions.color.color;colorsDefined = true; - } - if (newOptions.color.highlight !== undefined) { - parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; - } - if (newOptions.color.hover !== undefined) { - parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; - } - if (newOptions.color.inherit !== undefined) { - parentOptions.color.inherit = newOptions.color.inherit; - } - if (newOptions.color.opacity !== undefined) { - parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); - } + 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; }; })(); - if (newOptions.color.inherit === undefined && colorsDefined === true) { - parentOptions.color.inherit = false; - } - } - } else if (allowDeletion === true) { - parentOptions.color = undefined; - delete parentOptions.color; - } - } - }]); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - return Edge; - })(); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _sharedLabel = __webpack_require__(74); + + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + + var _edgesBezierEdgeDynamic = __webpack_require__(106); + + var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); + + var _edgesBezierEdgeStatic = __webpack_require__(107); + + var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); + + var _edgesStraightEdge = __webpack_require__(108); + + var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); + + var util = __webpack_require__(1); + + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ - exports['default'] = Edge; - module.exports = exports['default']; + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); -/***/ }, -/* 74 */ -/***/ function(module, exports, __webpack_require__) { + if (body === undefined) { + throw 'No body provided'; + } + this.options = util.bridgeObject(globalOptions); + this.body = body; - "use strict"; + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; - Object.defineProperty(exports, "__esModule", { - value: true - }); + this.baseWidth = this.options.width; + this.baseFontSize = this.options.font.size; - 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; }; })(); + this.from = undefined; // a node + this.to = undefined; // a node - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + this.edgeType = undefined; - var BarnesHutSolver = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); + this.connected = false; + + this.labelModule = new _sharedLabel2['default'](this.body, this.options); - this.body = body; - this.physicsBody = physicsBody; - this.barnesHutTree; this.setOptions(options); } - _createClass(BarnesHutSolver, [{ - key: "setOptions", - value: function setOptions(options) { - this.options = options; - this.thetaInversed = 1 / this.options.theta; - } - }, { - key: "solve", + _createClass(Edge, [{ + key: 'setOptions', /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit */ - value: function solve() { - if (this.options.gravitationalConstant != 0) { - var node; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var nodeCount = nodeIndices.length; - - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + value: function setOptions(options) { + if (!options) { + return; + } + this.colorDirty = true; - // for debugging - this.barnesHutTree = barnesHutTree; + Edge.parseOptions(this.options, options, true); - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); - } - } + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + options.value = parseInt(options.value); } - } - }, { - key: "_getForceContribution", - /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ - value: function _getForceContribution(parentBranch, node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx, dy, distance; + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); + // update label Module + this.updateLabelModule(); - // BarnesHutSolver condition - // original condition : s/d < theta = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.thetaInversed) { - this._calculateForces(distance, dx, dy, node, parentBranch); - } else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount === 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); - } else { - // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { - // if it is not self - this._calculateForces(distance, dx, dy, node, parentBranch); - } - } - } - } + var dataChanged = this.updateEdgeType(); + + // if anything has been updates, reset the selection width and the hover width + this._setInteractionWidths(); + + return dataChanged; } }, { - key: "_calculateForces", + key: 'updateLabelModule', /** - * Calculate the forces based on the distance. - * - * @param distance - * @param dx - * @param dy - * @param node - * @param parentBranch - * @private + * update the options in the label module */ - value: function _calculateForces(distance, dx, dy, node, parentBranch) { - // duplicate code to reduce function calls to speed up program - if (distance === 0) { - distance = 0.1 * Math.random(); - dx = distance; + value: function updateLabelModule() { + this.labelModule.setOptions(this.options, true); + if (this.labelModule.baseSize !== undefined) { + this.baseFontSize = this.labelModule.baseSize; } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; } }, { - key: "_formBarnesHutTree", + key: 'updateEdgeType', /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private + * update the edge type, set the options + * @returns {boolean} */ - value: function _formBarnesHutTree(nodes, nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX = -Number.MAX_VALUE, - maxY = -Number.MAX_VALUE; - - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.dynamic === true) { + changeInType = false; + } + if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.dynamic === false) { + changeInType = false; + } + if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { + changeInType = false; } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) { - minY -= 0.5 * sizeDiff; - maxY += 0.5 * sizeDiff; - } // xSize > ySize - else { - minX += 0.5 * sizeDiff; - maxX -= 0.5 * sizeDiff; - } // xSize < ySize - - var minimumTreeSize = 0.00001; - var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), - centerY = 0.5 * (minY + maxY); - // construct the barnesHutTree - var barnesHutTree = { - root: { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { - minX: centerX - halfRootSize, maxX: centerX + halfRootSize, - minY: centerY - halfRootSize, maxY: centerY + halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data: null }, - maxWidth: 0, - level: 0, - childrenCount: 4 + if (changeInType === true) { + dataChanged = this.edgeType.cleanup(); } - }; - this._splitBranch(barnesHutTree.root); + } - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root, node); + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.dynamic === true) { + dataChanged = true; + this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); + } else { + this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); } + } else { + // if nothing changes, we just set the options. + this.edgeType.setOptions(this.options); } - // make global - return barnesHutTree; + return dataChanged; } }, { - key: "_updateBranchMass", + key: 'togglePhysics', /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private + * Enable or disable the physics. + * @param status */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; + value: function togglePhysics(status) { + this.options.physics = status; + this.edgeType.togglePhysics(status); + } + }, { + key: 'connect', - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; + /** + * Connect an edge to its nodes + */ + value: function connect() { + this.disconnect(); - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); + } + } } }, { - key: "_placeInTree", + key: 'disconnect', /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private + * Disconnect an edge from its nodes */ - value: function _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; + } + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; } - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); + this.connected = false; + } + }, { + key: 'getTitle', + + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + value: function getTitle() { + return this.title; + } + }, { + key: 'isSelected', + + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + } + }, { + key: 'getValue', + + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; + } + }, { + key: 'setValueRange', + + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total + */ + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } + this.options.width = this.options.scaling.min + scale * widthDiff; } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); - } else { - // in SE - this._placeInRegion(parentBranch, node, "SE"); - } + this.options.width = this.baseWidth; + this.options.font.size = this.baseFontSize; + } + + this._setInteractionWidths(); + } + }, { + key: '_setInteractionWidths', + value: function _setInteractionWidths() { + if (typeof this.options.hoverWidth === 'function') { + this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); + } else { + this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; + } + + if (typeof this.options.selectionWidth === 'function') { + this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); + } else { + this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; } } }, { - key: "_placeInRegion", + key: 'draw', /** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - value: function _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { - case 0: - // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); - break; - case 1: - // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { - node.x += Math.random(); - node.y += Math.random(); + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); + } + }, { + key: 'drawArrows', + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); + } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); + } + } + }, { + key: 'drawLabel', + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); + + // if the label has to be rotated: + if (this.options.font.align !== 'horizontal') { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); + } + + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.shape.width > node1.shape.height) { + x = node1.x + node1.shape.width * 0.5; + y = node1.y - radius; } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); + x = node1.x + radius; + y = node1.y - node1.shape.height * 0.5; } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; + point = this._pointOnCircle(x, y, radius, 0.125); + this.labelModule.draw(ctx, point.x, point.y, selected); + } } } }, { - key: "_splitBranch", + key: 'isOverlappingWith', /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch - * @private + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge */ - value: function _splitBranch(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount === 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; - parentBranch.centerOfMass.x = 0; - parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch, "NW"); - this._insertRegion(parentBranch, "NE"); - this._insertRegion(parentBranch, "SW"); - this._insertRegion(parentBranch, "SE"); + value: function isOverlappingWith(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - if (containedNode != null) { - this._placeInTree(parentBranch, containedNode); + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return dist < distMax; + } else { + return false; } } }, { - key: "_insertRegion", + key: '_rotateForLabelAlignment', /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _insertRegion(parentBranch, region) { - var minX, maxX, minY, maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); + + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; } - parentBranch.children[region] = { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: { data: null }, - maxWidth: 0, - level: parentBranch.level + 1, - childrenCount: 0 - }; + ctx.rotate(angleInDegrees); } }, { - key: "_debug", - - //--------------------------- DEBUGGING BELOW ---------------------------// + key: '_pointOnCircle', /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point * @private */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root, ctx, color); - } + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; } }, { - key: "_drawBranch", + key: 'select', + value: function select() { + this.selected = true; + } + }, { + key: 'unselect', + value: function unselect() { + this.selected = false; + } + }], [{ + key: 'parseOptions', + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ - value: function _drawBranch(branch, ctx, color) { - if (color === undefined) { - color = "#FF0000"; - } + var fields = ['id', 'font', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; - if (branch.childrenCount === 4) { - this._drawBranch(branch.children.NW, ctx); - this._drawBranch(branch.children.NE, ctx); - this._drawBranch(branch.children.SE, ctx); - this._drawBranch(branch.children.SW, ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.minY); - ctx.stroke(); + // only deep extend the items in the field array. These do not have shorthand. + util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.maxY); - ctx.stroke(); + util.mergeOptions(parentOptions, newOptions, 'smooth'); + util.mergeOptions(parentOptions, newOptions, 'shadow'); - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.maxY); - ctx.stroke(); + if (newOptions.dashes !== undefined && newOptions.dashes !== null) { + parentOptions.dashes = newOptions.dashes; + } else if (allowDeletion === true) { + parentOptions.dashes = undefined; + delete parentOptions.dashes; + } - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.minY); - ctx.stroke(); + // set the scaling newOptions + if (newOptions.scaling !== undefined && newOptions.scaling !== null) { + if (newOptions.scaling.min !== undefined) { + parentOptions.scaling.min = newOptions.scaling.min; + } + if (newOptions.scaling.max !== undefined) { + parentOptions.scaling.max = newOptions.scaling.max; + } + util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); + } else if (allowDeletion === true) { + parentOptions.scaling = undefined; + delete parentOptions.scaling; + } - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ + // hanlde multiple input cases for arrows + if (newOptions.arrows !== undefined && newOptions.arrows !== null) { + if (typeof newOptions.arrows === 'string') { + var arrows = newOptions.arrows.toLowerCase(); + if (arrows.indexOf('to') != -1) { + parentOptions.arrows.to.enabled = true; + } + if (arrows.indexOf('middle') != -1) { + parentOptions.arrows.middle.enabled = true; + } + if (arrows.indexOf('from') != -1) { + parentOptions.arrows.from.enabled = true; + } + } else if (typeof newOptions.arrows === 'object') { + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); + } else { + throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); + } + } else if (allowDeletion === true) { + parentOptions.arrows = undefined; + delete parentOptions.arrows; + } + + // hanlde multiple input cases for color + if (newOptions.color !== undefined && newOptions.color !== null) { + if (util.isString(newOptions.color)) { + parentOptions.color.color = newOptions.color; + parentOptions.color.highlight = newOptions.color; + parentOptions.color.hover = newOptions.color; + parentOptions.color.inherit = false; + } else { + var colorsDefined = false; + if (newOptions.color.color !== undefined) { + parentOptions.color.color = newOptions.color.color;colorsDefined = true; + } + if (newOptions.color.highlight !== undefined) { + parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; + } + if (newOptions.color.hover !== undefined) { + parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; + } + if (newOptions.color.inherit !== undefined) { + parentOptions.color.inherit = newOptions.color.inherit; + } + if (newOptions.color.opacity !== undefined) { + parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); + } + + if (newOptions.color.inherit === undefined && colorsDefined === true) { + parentOptions.color.inherit = false; + } + } + } else if (allowDeletion === true) { + parentOptions.color = undefined; + delete parentOptions.color; + } } }]); - return BarnesHutSolver; + return Edge; })(); - exports["default"] = BarnesHutSolver; - module.exports = exports["default"]; + exports['default'] = Edge; + module.exports = exports['default']; /***/ }, -/* 75 */ +/* 76 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35114,289 +35474,474 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var RepulsionSolver = (function () { - function RepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, RepulsionSolver); + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); this.body = body; this.physicsBody = physicsBody; + this.barnesHutTree; this.setOptions(options); } - _createClass(RepulsionSolver, [{ + _createClass(BarnesHutSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; + this.thetaInversed = 1 / this.options.theta; } }, { key: "solve", /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. * * @private */ value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2; - - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; - - // approximation constants - var a = -2 / 3 / nodeDistance; - var b = 4 / 3; + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j - for (var i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (var j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + // for debugging + this.barnesHutTree = barnesHutTree; - // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. - if (distance === 0) { - distance = 0.1 * Math.random(); - dx = distance; + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); } + } + } + } + }, { + key: "_getForceContribution", - if (distance < 2 * nodeDistance) { - if (distance < 0.5 * nodeDistance) { - repulsingForce = 1; - } else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) - } - repulsingForce = repulsingForce / distance; + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; - fx = dx * repulsingForce; - fy = dy * repulsingForce; + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; + // BarnesHutSolver condition + // original condition : s/d < theta = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.thetaInversed) { + this._calculateForces(distance, dx, dy, node, parentBranch); + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount === 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } else { + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + this._calculateForces(distance, dx, dy, node, parentBranch); + } } } } } - }]); - - return RepulsionSolver; - })(); + }, { + key: "_calculateForces", - exports["default"] = RepulsionSolver; - module.exports = exports["default"]; + /** + * Calculate the forces based on the distance. + * + * @param distance + * @param dx + * @param dy + * @param node + * @param parentBranch + * @private + */ + value: function _calculateForces(distance, dx, dy, node, parentBranch) { + // duplicate code to reduce function calls to speed up program + if (distance === 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; -/***/ }, -/* 76 */ -/***/ function(module, exports, __webpack_require__) { + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + }, { + key: "_formBarnesHutTree", - "use strict"; + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; - Object.defineProperty(exports, "__esModule", { - value: true - }); + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; - 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; }; })(); + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); - var HierarchicalRepulsionSolver = (function () { - function HierarchicalRepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalRepulsionSolver); + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } - _createClass(HierarchicalRepulsionSolver, [{ - key: "setOptions", - value: function setOptions(options) { - this.options = options; + // make global + return barnesHutTree; } }, { - key: "solve", + key: "_updateBranchMass", /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. + * this updates the mass of a branch. this is increased by adding a node. * + * @param parentBranch + * @param node * @private */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; - // nodes only affect nodes on their level - if (node1.level === node2.level) { - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + } + }, { + key: "_placeInTree", - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); - } else { - repulsingForce = 0; - } - // normalize force with - if (distance === 0) { - distance = 0.01; - } else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); } } } - }]); - - return HierarchicalRepulsionSolver; - })(); - - exports["default"] = HierarchicalRepulsionSolver; - module.exports = exports["default"]; - -/***/ }, -/* 77 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - Object.defineProperty(exports, "__esModule", { - value: true - }); + }, { + key: "_placeInRegion", - 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; }; })(); + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { + node.x += Math.random(); + node.y += Math.random(); + } else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; + } + } + }, { + key: "_splitBranch", - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount === 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); - var SpringSolver = (function () { - function SpringSolver(body, physicsBody, options) { - _classCallCheck(this, SpringSolver); + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } + } + }, { + key: "_insertRegion", - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + value: function _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } - _createClass(SpringSolver, [{ - key: "setOptions", - value: function setOptions(options) { - this.options = options; + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; } }, { - key: "solve", + key: "_debug", + + //--------------------------- DEBUGGING BELOW ---------------------------// /** - * This function calculates the springforces on the nodes, accounting for the support nodes. + * This function is for debugging purposed, it draws the tree. * + * @param ctx + * @param color * @private */ - value: function solve() { - var edgeLength, edge; - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var edges = this.body.edges; - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true && edge.toId !== edge.fromId) { - // only calculate forces if nodes are in the same sector - if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { - if (edge.edgeType.via !== undefined) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - var node1 = edge.to; - var node2 = edge.edgeType.via; - var node3 = edge.from; + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } else { - // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use - // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. - edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; - this._calculateSpringForce(edge.from, edge.to, edgeLength); - } - } - } + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root, ctx, color); } } }, { - key: "_calculateSpringForce", + key: "_drawBranch", /** - * This is the code actually performing the calculation for the function above. + * This function is for debugging purposes. It draws the branches recursively. * - * @param node1 - * @param node2 - * @param edgeLength + * @param branch + * @param ctx + * @param color * @private */ - value: function _calculateSpringForce(node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } - dx = node1.x - node2.x; - dy = node1.y - node2.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance === 0 ? 0.01 : distance; + if (branch.childrenCount === 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); - fx = dx * springForce; - fy = dy * springForce; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); - // handle the case where one node is not part of the physcis - if (this.physicsBody.forces[node1.id] !== undefined) { - this.physicsBody.forces[node1.id].x += fx; - this.physicsBody.forces[node1.id].y += fy; - } + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); - if (this.physicsBody.forces[node2.id] !== undefined) { - this.physicsBody.forces[node2.id].x -= fx; - this.physicsBody.forces[node2.id].y -= fy; - } + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ } }]); - return SpringSolver; + return BarnesHutSolver; })(); - exports["default"] = SpringSolver; + exports["default"] = BarnesHutSolver; module.exports = exports["default"]; /***/ }, -/* 78 */ +/* 77 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35409,16 +35954,16 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var HierarchicalSpringSolver = (function () { - function HierarchicalSpringSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalSpringSolver); + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } - _createClass(HierarchicalSpringSolver, [{ + _createClass(RepulsionSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; @@ -35427,97 +35972,71 @@ return /******/ (function(modules) { // webpackBootstrap key: "solve", /** - * This function calculates the springforces on the nodes, accounting for the support nodes. + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. * * @private */ value: function solve() { - var edgeLength, edge; - var dx, dy, fx, fy, springForce, distance; - var edges = this.body.edges; - var factor = 0.5; + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; - var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodes = this.body.nodes; var nodeIndices = this.physicsBody.physicsNodeIndices; var forces = this.physicsBody.forces; - // initialize the spring force counters - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].springFx = 0; - forces[nodeId].springFy = 0; - } - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; - dx = edge.from.x - edge.to.x; - dy = edge.from.y - edge.to.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance === 0 ? 0.01 : distance; + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j + for (var i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (var j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; - fx = dx * springForce; - fy = dy * springForce; + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); - if (edge.to.level != edge.from.level) { - forces[edge.toId].springFx -= fx; - forces[edge.toId].springFy -= fy; - forces[edge.fromId].springFx += fx; - forces[edge.fromId].springFy += fy; - } else { - forces[edge.toId].x -= factor * fx; - forces[edge.toId].y -= factor * fy; - forces[edge.fromId].x += factor * fx; - forces[edge.fromId].y += factor * fy; + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance === 0) { + distance = 0.1 * Math.random(); + dx = distance; } - } - } - - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); - springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); - forces[nodeId].x += springFx; - forces[nodeId].y += springFy; - } + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1; + } else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - totalFx += forces[nodeId].x; - totalFy += forces[nodeId].y; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; + fx = dx * repulsingForce; + fy = dy * repulsingForce; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].x -= correctionFx; - forces[nodeId].y -= correctionFy; + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } } } }]); - return HierarchicalSpringSolver; + return RepulsionSolver; })(); - exports["default"] = HierarchicalSpringSolver; + exports["default"] = RepulsionSolver; module.exports = exports["default"]; /***/ }, -/* 79 */ +/* 78 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35530,493 +36049,417 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var CentralGravitySolver = (function () { - function CentralGravitySolver(body, physicsBody, options) { - _classCallCheck(this, CentralGravitySolver); + var HierarchicalRepulsionSolver = (function () { + function HierarchicalRepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalRepulsionSolver); this.body = body; this.physicsBody = physicsBody; this.setOptions(options); } - _createClass(CentralGravitySolver, [{ + _createClass(HierarchicalRepulsionSolver, [{ key: "setOptions", value: function setOptions(options) { this.options = options; } }, { key: "solve", - value: function solve() { - var dx, dy, distance, node, i; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - - var gravity = this.options.centralGravity; - var gravityForce = 0; - - for (i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - node = nodes[nodeId]; - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - gravityForce = distance === 0 ? 0 : gravity / distance; - forces[nodeId].x = dx * gravityForce; - forces[nodeId].y = dy * gravityForce; - } - } - }]); - - return CentralGravitySolver; - })(); - - exports["default"] = CentralGravitySolver; - module.exports = exports["default"]; - -/***/ }, -/* 80 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, - property = _x2, - receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _Node2 = __webpack_require__(71); + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - var _Node3 = _interopRequireDefault(_Node2); + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - /** - * - */ + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; - var Cluster = (function (_Node) { - function Cluster(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Cluster); + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; - _get(Object.getPrototypeOf(Cluster.prototype), 'constructor', this).call(this, options, body, imagelist, grouplist, globalOptions); + // nodes only affect nodes on their level + if (node1.level === node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); - this.isCluster = true; - this.containedNodes = {}; - this.containedEdges = {}; - } + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } else { + repulsingForce = 0; + } + // normalize force with + if (distance === 0) { + distance = 0.01; + } else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; - _inherits(Cluster, _Node); + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + } + }]); - return Cluster; - })(_Node3['default']); + return HierarchicalRepulsionSolver; + })(); - exports['default'] = Cluster; - module.exports = exports['default']; + exports["default"] = HierarchicalRepulsionSolver; + module.exports = exports["default"]; /***/ }, -/* 81 */ +/* 79 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + Object.defineProperty(exports, "__esModule", { value: true }); - 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; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(1); - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(45); - var keycharm = __webpack_require__(84); + 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; }; })(); - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - _classCallCheck(this, NavigationHandler); + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); this.body = body; - this.canvas = canvas; - - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; - this.touchTime = 0; - this.activated = false; - - 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.physicsBody = physicsBody; + this.setOptions(options); } - _createClass(NavigationHandler, [{ - key: 'setOptions', + _createClass(SpringSolver, [{ + key: "setOptions", value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); - } + this.options = options; } }, { - key: 'create', - value: function create() { - if (this.options.navigationButtons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); - } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); - } + key: "solve", - 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 = []; - } + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + value: function solve() { + var edgeLength, edge; + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var edges = this.body.edges; - this._navigationReleaseOverload = function () {}; + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true && edge.toId !== edge.fromId) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + if (edge.edgeType.via !== undefined) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + var node1 = edge.to; + var node2 = edge.edgeType.via; + var node3 = edge.from; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { - this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } else { + // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use + // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. + edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } } - - this.iconsCreated = false; } }, { - key: 'loadNavigationElements', + key: "_calculateSpringForce", /** - * 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. + * This is the code actually performing the calculation for the function above. * + * @param node1 + * @param node2 + * @param edgeLength * @private */ - value: function loadNavigationElements() { - this.cleanNavigation(); + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - this.navigationDOM = {}; - var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; - var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance === 0 ? 0.01 : distance; - this.navigationDOM['wrapper'] = document.createElement('div'); - this.navigationDOM['wrapper'].className = 'vis-navigation'; - this.canvas.frame.appendChild(this.navigationDOM['wrapper']); + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - 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]]); + fx = dx * springForce; + fy = dy * springForce; - 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])); - } + // handle the case where one node is not part of the physcis + if (this.physicsBody.forces[node1.id] !== undefined) { + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; + } - this.navigationHammers.push(hammer); + if (this.physicsBody.forces[node2.id] !== undefined) { + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; } + } + }]); + + return SpringSolver; + })(); + + exports["default"] = SpringSolver; + module.exports = exports["default"]; + +/***/ }, +/* 80 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + 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; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - 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]; - } + _createClass(HierarchicalSpringSolver, [{ + key: "setOptions", + value: function setOptions(options) { + this.options = options; } }, { - key: '_fit', + key: "solve", /** - * this stops all movement induced by the navigation buttons + * This function calculates the springforces on the nodes, accounting for the support nodes. * * @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(); - } - } - }, { - key: '_stopMovement', + value: function solve() { + var edgeLength, edge; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + var factor = 0.5; - /** - * this stops all movement induced by the navigation buttons - * - * @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'); - } - } - 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; - } - }, { - key: '_zoomOut', - value: function _zoomOut() { - this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; - } - }, { - key: 'configureKeyboardBindings', + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - /** - * bind all keys using keycharm. - */ - value: function configureKeyboardBindings() { - if (this.keycharm !== undefined) { - this.keycharm.destroy(); + // initialize the spring force counters + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; } - if (this.options.keyboard.enabled === true) { + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: true }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); - } + dx = edge.from.x - edge.to.x; + dy = edge.from.y - edge.to.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance === 0 ? 0.01 : distance; - this.keycharm.reset(); + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - if (this.activated === true) { - this.keycharm.bind('up', this.bindToRedraw.bind(this, '_moveUp'), 'keydown'); - this.keycharm.bind('down', this.bindToRedraw.bind(this, '_moveDown'), 'keydown'); - this.keycharm.bind('left', this.bindToRedraw.bind(this, '_moveLeft'), 'keydown'); - this.keycharm.bind('right', this.bindToRedraw.bind(this, '_moveRight'), 'keydown'); - this.keycharm.bind('=', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); - this.keycharm.bind('num+', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); - this.keycharm.bind('num-', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); - this.keycharm.bind('-', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); - this.keycharm.bind('[', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); - this.keycharm.bind(']', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); - this.keycharm.bind('pageup', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); - this.keycharm.bind('pagedown', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); + fx = dx * springForce; + fy = dy * springForce; - this.keycharm.bind('up', this.unbindFromRedraw.bind(this, '_moveUp'), 'keyup'); - this.keycharm.bind('down', this.unbindFromRedraw.bind(this, '_moveDown'), 'keyup'); - this.keycharm.bind('left', this.unbindFromRedraw.bind(this, '_moveLeft'), 'keyup'); - this.keycharm.bind('right', this.unbindFromRedraw.bind(this, '_moveRight'), 'keyup'); - this.keycharm.bind('=', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); - this.keycharm.bind('num+', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); - this.keycharm.bind('num-', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); - this.keycharm.bind('-', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); - this.keycharm.bind('[', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); - this.keycharm.bind(']', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); - this.keycharm.bind('pageup', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); - this.keycharm.bind('pagedown', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; + } else { + forces[edge.toId].x -= factor * fx; + forces[edge.toId].y -= factor * fy; + forces[edge.fromId].x += factor * fx; + forces[edge.fromId].y += factor * fy; + } } } + + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); + springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); + + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; + } + + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; + } } }]); - return NavigationHandler; + return HierarchicalSpringSolver; })(); - exports['default'] = NavigationHandler; - module.exports = exports['default']; + exports["default"] = HierarchicalSpringSolver; + module.exports = exports["default"]; /***/ }, -/* 82 */ +/* 81 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + Object.defineProperty(exports, "__esModule", { value: true }); - 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; }; })(); + 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; }; })(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. - */ + var CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _createClass(CentralGravitySolver, [{ + key: "setOptions", + value: function setOptions(options) { + this.options = options; + } + }, { + key: "solve", + value: function solve() { + var dx, dy, distance, node, i; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + var gravity = this.options.centralGravity; + var gravityForce = 0; + + for (i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + gravityForce = distance === 0 ? 0 : gravity / distance; + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; + } + } + }]); + + return CentralGravitySolver; + })(); + + exports["default"] = CentralGravitySolver; + module.exports = exports["default"]; - var Popup = (function () { - function Popup(container) { - _classCallCheck(this, Popup); +/***/ }, +/* 82 */ +/***/ function(module, exports, __webpack_require__) { - this.container = container; + 'use strict'; - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + Object.defineProperty(exports, '__esModule', { + value: true + }); - // create the frame - this.frame = document.createElement('div'); - this.frame.className = 'vis-network-tooltip'; - this.container.appendChild(this.frame); - } + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, + property = _x2, + receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - _createClass(Popup, [{ - key: 'setPosition', + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window - */ - value: function setPosition(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); - } - }, { - key: 'setText', + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content - */ - value: function setText(content) { - if (content instanceof Element) { - this.frame.innerHTML = ''; - this.frame.appendChild(content); - } else { - this.frame.innerHTML = content; // string containing text or HTML - } - } - }, { - key: 'show', + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - /** - * Show the popup window - * @param {boolean} [doShow] Show or hide the window - */ - value: function show(doShow) { - if (doShow === undefined) { - doShow = true; - } + var _Node2 = __webpack_require__(73); - if (doShow === true) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + var _Node3 = _interopRequireDefault(_Node2); - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; - } + /** + * + */ - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; - } - if (left < this.padding) { - left = this.padding; - } + var Cluster = (function (_Node) { + function Cluster(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Cluster); - this.frame.style.left = left + 'px'; - this.frame.style.top = top + 'px'; - this.frame.style.visibility = 'visible'; - this.hidden = false; - } else { - this.hide(); - } - } - }, { - key: 'hide', + _get(Object.getPrototypeOf(Cluster.prototype), 'constructor', this).call(this, options, body, imagelist, grouplist, globalOptions); - /** - * Hide the popup window - */ - value: function hide() { - this.hidden = true; - this.frame.style.visibility = 'hidden'; - } - }]); + this.isCluster = true; + this.containedNodes = {}; + this.containedEdges = {}; + } - return Popup; - })(); + _inherits(Cluster, _Node); - exports['default'] = Popup; + return Cluster; + })(_Node3['default']); + + exports['default'] = Cluster; module.exports = exports['default']; /***/ }, @@ -36033,574 +36476,391 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(45); var util = __webpack_require__(1); + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(48); + var keycharm = __webpack_require__(85); - var ColorPicker = (function () { - function ColorPicker() { - var pixelRatio = arguments[0] === undefined ? 1 : arguments[0]; - - _classCallCheck(this, ColorPicker); - - this.pixelRatio = pixelRatio; - this.generated = false; - this.centerCoordinates = { x: 289 / 2, y: 289 / 2 }; - this.r = 289 * 0.49; - this.color = { r: 255, g: 255, b: 255, a: 1 }; - this.hueCircle = undefined; - this.initialColor = { r: 255, g: 255, b: 255, a: 1 }; - this.previousColor = undefined; - this.applied = false; + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; - // bound by - this.updateCallback = function () {}; + _classCallCheck(this, NavigationHandler); - // create all DOM elements - this._create(); - } + this.body = body; + this.canvas = canvas; - _createClass(ColorPicker, [{ - key: 'insertTo', + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; - /** - * this inserts the colorPicker into a div from the DOM - * @param container - */ - value: function insertTo(container) { - if (this.hammer !== undefined) { - this.hammer.destroy(); - this.hammer = 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.container = container; - this.container.appendChild(this.frame); - this._bindHammer(); + }); - this._setSize(); - } - }, { - key: 'setCallback', + this.options = {}; + } - /** - * the callback is executed on apply and save. Bind it to the application - * @param callback - */ - value: function setCallback(callback) { - if (typeof callback === 'function') { - this.updateCallback = callback; - } else { - throw new Error('Function attempted to set as colorPicker callback is not a function.'); - } - } - }, { - key: '_isColorString', - value: function _isColorString(color) { - var htmlColors = { black: '#000000', navy: '#000080', darkblue: '#00008B', mediumblue: '#0000CD', blue: '#0000FF', darkgreen: '#006400', green: '#008000', teal: '#008080', darkcyan: '#008B8B', deepskyblue: '#00BFFF', darkturquoise: '#00CED1', mediumspringgreen: '#00FA9A', lime: '#00FF00', springgreen: '#00FF7F', aqua: '#00FFFF', cyan: '#00FFFF', midnightblue: '#191970', dodgerblue: '#1E90FF', lightseagreen: '#20B2AA', forestgreen: '#228B22', seagreen: '#2E8B57', darkslategray: '#2F4F4F', limegreen: '#32CD32', mediumseagreen: '#3CB371', turquoise: '#40E0D0', royalblue: '#4169E1', steelblue: '#4682B4', darkslateblue: '#483D8B', mediumturquoise: '#48D1CC', indigo: '#4B0082', darkolivegreen: '#556B2F', cadetblue: '#5F9EA0', cornflowerblue: '#6495ED', mediumaquamarine: '#66CDAA', dimgray: '#696969', slateblue: '#6A5ACD', olivedrab: '#6B8E23', slategray: '#708090', lightslategray: '#778899', mediumslateblue: '#7B68EE', lawngreen: '#7CFC00', chartreuse: '#7FFF00', aquamarine: '#7FFFD4', maroon: '#800000', purple: '#800080', olive: '#808000', gray: '#808080', skyblue: '#87CEEB', lightskyblue: '#87CEFA', blueviolet: '#8A2BE2', darkred: '#8B0000', darkmagenta: '#8B008B', saddlebrown: '#8B4513', darkseagreen: '#8FBC8F', lightgreen: '#90EE90', mediumpurple: '#9370D8', darkviolet: '#9400D3', palegreen: '#98FB98', darkorchid: '#9932CC', yellowgreen: '#9ACD32', sienna: '#A0522D', brown: '#A52A2A', darkgray: '#A9A9A9', lightblue: '#ADD8E6', greenyellow: '#ADFF2F', paleturquoise: '#AFEEEE', lightsteelblue: '#B0C4DE', powderblue: '#B0E0E6', firebrick: '#B22222', darkgoldenrod: '#B8860B', mediumorchid: '#BA55D3', rosybrown: '#BC8F8F', darkkhaki: '#BDB76B', silver: '#C0C0C0', mediumvioletred: '#C71585', indianred: '#CD5C5C', peru: '#CD853F', chocolate: '#D2691E', tan: '#D2B48C', lightgrey: '#D3D3D3', palevioletred: '#D87093', thistle: '#D8BFD8', orchid: '#DA70D6', goldenrod: '#DAA520', crimson: '#DC143C', gainsboro: '#DCDCDC', plum: '#DDA0DD', burlywood: '#DEB887', lightcyan: '#E0FFFF', lavender: '#E6E6FA', darksalmon: '#E9967A', violet: '#EE82EE', palegoldenrod: '#EEE8AA', lightcoral: '#F08080', khaki: '#F0E68C', aliceblue: '#F0F8FF', honeydew: '#F0FFF0', azure: '#F0FFFF', sandybrown: '#F4A460', wheat: '#F5DEB3', beige: '#F5F5DC', whitesmoke: '#F5F5F5', mintcream: '#F5FFFA', ghostwhite: '#F8F8FF', salmon: '#FA8072', antiquewhite: '#FAEBD7', linen: '#FAF0E6', lightgoldenrodyellow: '#FAFAD2', oldlace: '#FDF5E6', red: '#FF0000', fuchsia: '#FF00FF', magenta: '#FF00FF', deeppink: '#FF1493', orangered: '#FF4500', tomato: '#FF6347', hotpink: '#FF69B4', coral: '#FF7F50', darkorange: '#FF8C00', lightsalmon: '#FFA07A', orange: '#FFA500', lightpink: '#FFB6C1', pink: '#FFC0CB', gold: '#FFD700', peachpuff: '#FFDAB9', navajowhite: '#FFDEAD', moccasin: '#FFE4B5', bisque: '#FFE4C4', mistyrose: '#FFE4E1', blanchedalmond: '#FFEBCD', papayawhip: '#FFEFD5', lavenderblush: '#FFF0F5', seashell: '#FFF5EE', cornsilk: '#FFF8DC', lemonchiffon: '#FFFACD', floralwhite: '#FFFAF0', snow: '#FFFAFA', yellow: '#FFFF00', lightyellow: '#FFFFE0', ivory: '#FFFFF0', white: '#FFFFFF' }; - if (typeof color === 'string') { - return htmlColors[color]; + _createClass(NavigationHandler, [{ + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + this.options = options; + this.create(); } } }, { - key: 'setColor', - - /** - * Set the color of the colorPicker - * Supported formats: - * 'red' --> HTML color string - * '#ffffff' --> hex string - * 'rbg(255,255,255)' --> rgb string - * 'rgba(255,255,255,1.0)' --> rgba string - * {r:255,g:255,b:255} --> rgb object - * {r:255,g:255,b:255,a:1.0} --> rgba object - * @param color - * @param setInitial - */ - value: function setColor(color) { - var setInitial = arguments[1] === undefined ? true : arguments[1]; - - if (color === 'none') { - return; - } - - var rgba = undefined; - - // if a html color shorthand is used, convert to hex - var htmlColor = this._isColorString(color); - if (htmlColor !== undefined) { - color = htmlColor; - } - - // check format - if (util.isString(color) === true) { - if (util.isValidRGB(color) === true) { - var rgbaArray = color.substr(4).substr(0, color.length - 5).split(','); - rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: 1 }; - } else if (util.isValidRGBA(color) === true) { - var rgbaArray = color.substr(5).substr(0, color.length - 6).split(','); - rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: rgbaArray[3] }; - } else if (util.isValidHex(color) === true) { - var rgbObj = util.hexToRGB(color); - rgba = { r: rgbObj.r, g: rgbObj.g, b: rgbObj.b, a: 1 }; - } - } else { - if (color instanceof Object) { - if (color.r !== undefined && color.g !== undefined && color.b !== undefined) { - var alpha = color.a !== undefined ? color.a : '1.0'; - rgba = { r: color.r, g: color.g, b: color.b, a: alpha }; - } + key: 'create', + value: function create() { + if (this.options.navigationButtons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); } - // set color - if (rgba === undefined) { - throw new Error('Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: ' + JSON.stringify(color)); - } else { - this._setColor(rgba, setInitial); - } + this.configureKeyboardBindings(); } }, { - key: 'show', + 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 = []; + } - /** - * this shows the color picker at a location. The hue circle is constructed once and stored. - * @param x - * @param y - */ - value: function show(x, y) { - this.applied = false; - this.frame.style.display = 'block'; - this.frame.style.top = y + 'px'; - this.frame.style.left = x + 'px'; - this._generateHueCircle(); + this._navigationReleaseOverload = function () {}; + + // 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: '_hide', - - // ------------------------------------------ PRIVATE ----------------------------- // + key: 'loadNavigationElements', /** - * Hide the picker. Is called by the cancel button. - * Optional boolean to store the previous color for easy access later on. - * @param storePrevious + * 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 _hide() { - var storePrevious = arguments[0] === undefined ? true : arguments[0]; + value: function loadNavigationElements() { + this.cleanNavigation(); - // store the previous color for next time; - if (storePrevious === true) { - this.previousColor = util.extend({}, this.color); - } + this.navigationDOM = {}; + var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; + var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; - if (this.applied === true) { - this.updateCallback(this.initialColor); + this.navigationDOM['wrapper'] = document.createElement('div'); + this.navigationDOM['wrapper'].className = 'vis-navigation'; + this.canvas.frame.appendChild(this.navigationDOM['wrapper']); + + 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 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])); + } + + this.navigationHammers.push(hammer); } - this.frame.style.display = 'none'; + this.iconsCreated = true; } }, { - key: '_save', - - /** - * bound to the save button. Saves and hides. - * @private - */ - value: function _save() { - this.updateCallback(this.color); - this.applied = false; - this._hide(); + 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: '_apply', - - /** - * Bound to apply button. Saves but does not close. Is undone by the cancel button. - * @private - */ - value: function _apply() { - this.applied = true; - this.updateCallback(this.color); - this._updatePicker(this.color); + 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: '_loadLast', + key: '_fit', /** - * load the color from the previous session. + * this stops all movement induced by the navigation buttons + * * @private */ - value: function _loadLast() { - if (this.previousColor !== undefined) { - this.setColor(this.previousColor, false); - } else { - alert('There is no last color to load...'); + 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(); } } }, { - key: '_setColor', + key: '_stopMovement', /** - * set the color, place the picker - * @param rgba - * @param setInitial + * this stops all movement induced by the navigation buttons + * * @private */ - value: function _setColor(rgba) { - var setInitial = arguments[1] === undefined ? true : arguments[1]; - - // store the initial color - if (setInitial === true) { - this.initialColor = util.extend({}, rgba); + 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.color = rgba; - var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); - - var angleConvert = 2 * Math.PI; - var radius = this.r * hsv.s; - var x = this.centerCoordinates.x + radius * Math.sin(angleConvert * hsv.h); - var y = this.centerCoordinates.y + radius * Math.cos(angleConvert * hsv.h); - - this.colorPickerSelector.style.left = x - 0.5 * this.colorPickerSelector.clientWidth + 'px'; - this.colorPickerSelector.style.top = y - 0.5 * this.colorPickerSelector.clientHeight + 'px'; - - this._updatePicker(rgba); + this.boundFunctions = {}; } }, { - key: '_setOpacity', - - /** - * bound to opacity control - * @param value - * @private - */ - value: function _setOpacity(value) { - this.color.a = value / 100; - this._updatePicker(this.color); + key: '_moveUp', + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; } }, { - key: '_setBrightness', - - /** - * bound to brightness control - * @param value - * @private - */ - value: function _setBrightness(value) { - var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); - hsv.v = value / 100; - var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); - rgba['a'] = this.color.a; - this.color = rgba; - this._updatePicker(); + key: '_moveDown', + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; } }, { - key: '_updatePicker', - - /** - * update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing. - * @param rgba - * @private - */ - value: function _updatePicker() { - var rgba = arguments[0] === undefined ? this.color : arguments[0]; - - var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); - var ctx = this.colorPickerCanvas.getContext('2d'); - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.colorPickerCanvas.clientWidth; - var h = this.colorPickerCanvas.clientHeight; - ctx.clearRect(0, 0, w, h); - - ctx.putImageData(this.hueCircle, 0, 0); - ctx.fillStyle = 'rgba(0,0,0,' + (1 - hsv.v) + ')'; - ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); - ctx.fill(); - - this.brightnessRange.value = 100 * hsv.v; - this.opacityRange.value = 100 * rgba.a; - - this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; - this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + key: '_moveLeft', + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; } }, { - key: '_setSize', - - /** - * used by create to set the size of the canvas. - * @private - */ - value: function _setSize() { - this.colorPickerCanvas.style.width = '100%'; - this.colorPickerCanvas.style.height = '100%'; - - this.colorPickerCanvas.width = 289 * this.pixelRatio; - this.colorPickerCanvas.height = 289 * this.pixelRatio; + key: '_moveRight', + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; } }, { - key: '_create', + key: '_zoomIn', + value: function _zoomIn() { + this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; + } + }, { + key: '_zoomOut', + value: function _zoomOut() { + this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; + } + }, { + key: 'configureKeyboardBindings', /** - * create all dom elements - * TODO: cleanup, lots of similar dom elements - * @private + * bind all keys using keycharm. */ - value: function _create() { - this.frame = document.createElement('div'); - this.frame.className = 'vis-color-picker'; - - this.colorPickerDiv = document.createElement('div'); - this.colorPickerSelector = document.createElement('div'); - this.colorPickerSelector.className = 'vis-selector'; - this.colorPickerDiv.appendChild(this.colorPickerSelector); - - this.colorPickerCanvas = document.createElement('canvas'); - this.colorPickerDiv.appendChild(this.colorPickerCanvas); - - if (!this.colorPickerCanvas.getContext) { - var noCanvas = document.createElement('DIV'); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold'; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.colorPickerCanvas.appendChild(noCanvas); - } else { - var ctx = this.colorPickerCanvas.getContext('2d'); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - - this.colorPickerCanvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + value: function configureKeyboardBindings() { + if (this.keycharm !== undefined) { + this.keycharm.destroy(); } - this.colorPickerDiv.className = 'vis-color'; + if (this.options.keyboard.enabled === true) { - this.opacityDiv = document.createElement('div'); - this.opacityDiv.className = 'vis-opacity'; + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: true }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); + } - this.brightnessDiv = document.createElement('div'); - this.brightnessDiv.className = 'vis-brightness'; + this.keycharm.reset(); - this.arrowDiv = document.createElement('div'); - this.arrowDiv.className = 'vis-arrow'; + if (this.activated === true) { + this.keycharm.bind('up', this.bindToRedraw.bind(this, '_moveUp'), 'keydown'); + this.keycharm.bind('down', this.bindToRedraw.bind(this, '_moveDown'), 'keydown'); + this.keycharm.bind('left', this.bindToRedraw.bind(this, '_moveLeft'), 'keydown'); + this.keycharm.bind('right', this.bindToRedraw.bind(this, '_moveRight'), 'keydown'); + this.keycharm.bind('=', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); + this.keycharm.bind('num+', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); + this.keycharm.bind('num-', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); + this.keycharm.bind('-', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); + this.keycharm.bind('[', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); + this.keycharm.bind(']', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); + this.keycharm.bind('pageup', this.bindToRedraw.bind(this, '_zoomIn'), 'keydown'); + this.keycharm.bind('pagedown', this.bindToRedraw.bind(this, '_zoomOut'), 'keydown'); - this.opacityRange = document.createElement('input'); - this.opacityRange.type = 'range'; - this.opacityRange.min = '0'; - this.opacityRange.max = '100'; - this.opacityRange.value = '100'; - this.opacityRange.className = 'vis-range'; + this.keycharm.bind('up', this.unbindFromRedraw.bind(this, '_moveUp'), 'keyup'); + this.keycharm.bind('down', this.unbindFromRedraw.bind(this, '_moveDown'), 'keyup'); + this.keycharm.bind('left', this.unbindFromRedraw.bind(this, '_moveLeft'), 'keyup'); + this.keycharm.bind('right', this.unbindFromRedraw.bind(this, '_moveRight'), 'keyup'); + this.keycharm.bind('=', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); + this.keycharm.bind('num+', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); + this.keycharm.bind('num-', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); + this.keycharm.bind('-', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); + this.keycharm.bind('[', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); + this.keycharm.bind(']', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); + this.keycharm.bind('pageup', this.unbindFromRedraw.bind(this, '_zoomIn'), 'keyup'); + this.keycharm.bind('pagedown', this.unbindFromRedraw.bind(this, '_zoomOut'), 'keyup'); + } + } + } + }]); - this.brightnessRange = document.createElement('input'); - this.brightnessRange.type = 'range'; - this.brightnessRange.min = '0'; - this.brightnessRange.max = '100'; - this.brightnessRange.value = '100'; - this.brightnessRange.className = 'vis-range'; + return NavigationHandler; + })(); - this.opacityDiv.appendChild(this.opacityRange); - this.brightnessDiv.appendChild(this.brightnessRange); + exports['default'] = NavigationHandler; + module.exports = exports['default']; - var me = this; - this.opacityRange.onchange = function () { - me._setOpacity(this.value); - }; - this.opacityRange.oninput = function () { - me._setOpacity(this.value); - }; - this.brightnessRange.onchange = function () { - me._setBrightness(this.value); - }; - this.brightnessRange.oninput = function () { - me._setBrightness(this.value); - }; +/***/ }, +/* 84 */ +/***/ function(module, exports, __webpack_require__) { - this.brightnessLabel = document.createElement('div'); - this.brightnessLabel.className = 'vis-label vis-brightness'; - this.brightnessLabel.innerHTML = 'brightness:'; + 'use strict'; - this.opacityLabel = document.createElement('div'); - this.opacityLabel.className = 'vis-label vis-opacity'; - this.opacityLabel.innerHTML = 'opacity:'; + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.newColorDiv = document.createElement('div'); - this.newColorDiv.className = 'vis-new-color'; - this.newColorDiv.innerHTML = 'new'; + 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; }; })(); - this.initialColorDiv = document.createElement('div'); - this.initialColorDiv.className = 'vis-initial-color'; - this.initialColorDiv.innerHTML = 'initial'; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - this.cancelButton = document.createElement('div'); - this.cancelButton.className = 'vis-button vis-cancel'; - this.cancelButton.innerHTML = 'cancel'; - this.cancelButton.onclick = this._hide.bind(this, false); + /** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. + */ - this.applyButton = document.createElement('div'); - this.applyButton.className = 'vis-button vis-apply'; - this.applyButton.innerHTML = 'apply'; - this.applyButton.onclick = this._apply.bind(this); + var Popup = (function () { + function Popup(container) { + _classCallCheck(this, Popup); - this.saveButton = document.createElement('div'); - this.saveButton.className = 'vis-button vis-save'; - this.saveButton.innerHTML = 'save'; - this.saveButton.onclick = this._save.bind(this); + this.container = container; - this.loadButton = document.createElement('div'); - this.loadButton.className = 'vis-button vis-load'; - this.loadButton.innerHTML = 'load last'; - this.loadButton.onclick = this._loadLast.bind(this); + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - this.frame.appendChild(this.colorPickerDiv); - this.frame.appendChild(this.arrowDiv); - this.frame.appendChild(this.brightnessLabel); - this.frame.appendChild(this.brightnessDiv); - this.frame.appendChild(this.opacityLabel); - this.frame.appendChild(this.opacityDiv); - this.frame.appendChild(this.newColorDiv); - this.frame.appendChild(this.initialColorDiv); + // create the frame + this.frame = document.createElement('div'); + this.frame.className = 'vis-network-tooltip'; + this.container.appendChild(this.frame); + } - this.frame.appendChild(this.cancelButton); - this.frame.appendChild(this.applyButton); - this.frame.appendChild(this.saveButton); - this.frame.appendChild(this.loadButton); - } - }, { - key: '_bindHammer', + _createClass(Popup, [{ + key: 'setPosition', /** - * bind hammer to the color picker - * @private + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window */ - value: function _bindHammer() { - var _this = this; - - this.drag = {}; - this.pinch = {}; - this.hammer = new Hammer(this.colorPickerCanvas); - this.hammer.get('pinch').set({ enable: true }); - - hammerUtil.onTouch(this.hammer, function (event) { - _this._moveSelector(event); - }); - this.hammer.on('tap', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panstart', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panmove', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panend', function (event) { - _this._moveSelector(event); - }); + value: function setPosition(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); } }, { - key: '_generateHueCircle', - - /** - * generate the hue circle. This is relatively heavy (200ms) and is done only once on the first time it is shown. - * @private - */ - value: function _generateHueCircle() { - if (this.generated === false) { - var ctx = this.colorPickerCanvas.getContext('2d'); - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.colorPickerCanvas.clientWidth; - var h = this.colorPickerCanvas.clientHeight; - ctx.clearRect(0, 0, w, h); - - // draw hue circle - var x = undefined, - y = undefined, - hue = undefined, - sat = undefined; - this.centerCoordinates = { x: w * 0.5, y: h * 0.5 }; - this.r = 0.49 * w; - var angleConvert = 2 * Math.PI / 360; - var hfac = 1 / 360; - var sfac = 1 / this.r; - var rgb = undefined; - for (hue = 0; hue < 360; hue++) { - for (sat = 0; sat < this.r; sat++) { - x = this.centerCoordinates.x + sat * Math.sin(angleConvert * hue); - y = this.centerCoordinates.y + sat * Math.cos(angleConvert * hue); - rgb = util.HSVToRGB(hue * hfac, sat * sfac, 1); - ctx.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; - ctx.fillRect(x - 0.5, y - 0.5, 2, 2); - } - } - ctx.strokeStyle = 'rgba(0,0,0,1)'; - ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); - ctx.stroke(); + key: 'setText', - this.hueCircle = ctx.getImageData(0, 0, w, h); + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + value: function setText(content) { + if (content instanceof Element) { + this.frame.innerHTML = ''; + this.frame.appendChild(content); + } else { + this.frame.innerHTML = content; // string containing text or HTML } - this.generated = true; } }, { - key: '_moveSelector', + key: 'show', /** - * move the selector. This is called by hammer functions. - * - * @param event - * @private + * Show the popup window + * @param {boolean} [doShow] Show or hide the window */ - value: function _moveSelector(event) { - var rect = this.colorPickerDiv.getBoundingClientRect(); - var left = event.center.x - rect.left; - var top = event.center.y - rect.top; - - var centerY = 0.5 * this.colorPickerDiv.clientHeight; - var centerX = 0.5 * this.colorPickerDiv.clientWidth; - - var x = left - centerX; - var y = top - centerY; + value: function show(doShow) { + if (doShow === undefined) { + doShow = true; + } - var angle = Math.atan2(x, y); - var radius = 0.98 * Math.min(Math.sqrt(x * x + y * y), centerX); + if (doShow === true) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - var newTop = Math.cos(angle) * radius + centerY; - var newLeft = Math.sin(angle) * radius + centerX; + var top = this.y - height; + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - this.colorPickerSelector.style.top = newTop - 0.5 * this.colorPickerSelector.clientHeight + 'px'; - this.colorPickerSelector.style.left = newLeft - 0.5 * this.colorPickerSelector.clientWidth + 'px'; + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } - // set color - var h = angle / (2 * Math.PI); - h = h < 0 ? h + 1 : h; - var s = radius / this.r; - var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); - hsv.h = h; - hsv.s = s; - var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); - rgba['a'] = this.color.a; - this.color = rgba; + this.frame.style.left = left + 'px'; + this.frame.style.top = top + 'px'; + this.frame.style.visibility = 'visible'; + this.hidden = false; + } else { + this.hide(); + } + } + }, { + key: 'hide', - // update previews - this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; - this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + /** + * Hide the popup window + */ + value: function hide() { + this.hidden = true; + this.frame.style.visibility = 'hidden'; } }]); - return ColorPicker; + return Popup; })(); - exports['default'] = ColorPicker; + exports['default'] = Popup; module.exports = exports['default']; /***/ }, -/* 84 */ +/* 85 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; @@ -36798,7 +37058,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 85 */ +/* 86 */ /***/ function(module, exports, __webpack_require__) { function webpackContext(req) { @@ -36807,11 +37067,11 @@ return /******/ (function(modules) { // webpackBootstrap webpackContext.keys = function() { return []; }; webpackContext.resolve = webpackContext; module.exports = webpackContext; - webpackContext.id = 85; + webpackContext.id = 86; /***/ }, -/* 86 */ +/* 87 */ /***/ function(module, exports, __webpack_require__) { module.exports = function(module) { @@ -36827,7 +37087,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 87 */ +/* 88 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; @@ -36835,7 +37095,7 @@ return /******/ (function(modules) { // webpackBootstrap /* WEBPACK VAR INJECTION */}.call(exports, {})) /***/ }, -/* 88 */ +/* 89 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36856,7 +37116,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilNodeBase = __webpack_require__(103); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); @@ -36936,7 +37196,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 89 */ +/* 90 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36957,7 +37217,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(106); + var _utilCircleImageBase = __webpack_require__(104); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); @@ -37020,7 +37280,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 90 */ +/* 91 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37041,7 +37301,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(106); + var _utilCircleImageBase = __webpack_require__(104); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); @@ -37123,7 +37383,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 91 */ +/* 92 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37144,7 +37404,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilNodeBase = __webpack_require__(103); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); @@ -37223,7 +37483,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 92 */ +/* 93 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37244,7 +37504,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37283,7 +37543,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 93 */ +/* 94 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37304,7 +37564,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37343,7 +37603,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 94 */ +/* 95 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37364,7 +37624,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilNodeBase = __webpack_require__(103); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); @@ -37446,7 +37706,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 95 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37467,7 +37727,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilNodeBase = __webpack_require__(103); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); @@ -37556,7 +37816,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 96 */ +/* 97 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37577,7 +37837,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(106); + var _utilCircleImageBase = __webpack_require__(104); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); @@ -37636,7 +37896,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 97 */ +/* 98 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37657,7 +37917,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37697,7 +37957,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 98 */ +/* 99 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37718,7 +37978,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37757,7 +38017,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 99 */ +/* 100 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37778,7 +38038,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilNodeBase = __webpack_require__(103); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); @@ -37837,7 +38097,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 100 */ +/* 101 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37858,7 +38118,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37897,7 +38157,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 101 */ +/* 102 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37918,7 +38178,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(107); + var _utilShapeBase = __webpack_require__(105); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); @@ -37957,7 +38217,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 102 */ +/* 103 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37968,154 +38228,62 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, - property = _x2, - receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - - var _utilBezierEdgeBase = __webpack_require__(108); - - var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); - - var BezierEdgeDynamic = (function (_BezierEdgeBase) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); - - //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below - } - - _inherits(BezierEdgeDynamic, _BezierEdgeBase); - - _createClass(BezierEdgeDynamic, [{ - key: 'setOptions', - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; - this.setupSupportNode(); - - // fix weird behaviour - if (this.from.id === this.to.id) { - this.via.setOptions({ physics: false }); - } else { - this.via.setOptions({ physics: true }); - } - } - }, { - key: 'cleanup', - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; - return true; - } - return false; - } - }, { - key: 'togglePhysics', - value: function togglePhysics(status) { - this.via.setOptions({ physics: status }); - } - }, { - key: 'setupSupportNode', - - /** - * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but - * are used for the force calculation. - * - * The changed data is not called, if needed, it is returned by the main edge constructor. - * @private - */ - value: function setupSupportNode() { - if (this.via === undefined) { - var nodeId = 'edgeId:' + this.id; - var node = this.body.functions.createNode({ - id: nodeId, - shape: 'circle', - physics: true, - hidden: true - }); - this.body.nodes[nodeId] = node; - this.via = node; - this.via.parentEdgeId = this.id; - this.positionBezierNode(); - } - } - }, { - key: 'positionBezierNode', - value: function positionBezierNode() { - if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== undefined) { - this.via.x = 0; - this.via.y = 0; - } - } - }, { - key: '_line', - - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - // draw shadow if enabled - this.enableShadow(ctx); - ctx.stroke(); - this.disableShadow(ctx); - return this.via; - } - }, { - key: 'getPoint', + var NodeBase = (function () { + function NodeBase(options, body, labelModule) { + _classCallCheck(this, NodeBase); - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.top = undefined; + this.left = undefined; + this.height = undefined; + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + } - return { x: x, y: y }; + _createClass(NodeBase, [{ + key: 'setOptions', + value: function setOptions(options) { + this.options = options; } }, { - key: '_findBorderPosition', - value: function _findBorderPosition(nearNode, ctx) { - return this._findBorderPositionBezier(nearNode, ctx, this.via); + key: '_distanceToBorder', + value: function _distanceToBorder(angle) { + var borderWidth = 1; + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; } }, { - key: '_getDistanceToEdge', - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); + key: 'enableShadow', + value: function enableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = this.options.shadow.size; + ctx.shadowOffsetX = this.options.shadow.x; + ctx.shadowOffsetY = this.options.shadow.y; + } + } + }, { + key: 'disableShadow', + value: function disableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0)'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } } }]); - return BezierEdgeDynamic; - })(_utilBezierEdgeBase2['default']); + return NodeBase; + })(); - exports['default'] = BezierEdgeDynamic; + exports['default'] = NodeBase; module.exports = exports['default']; /***/ }, -/* 103 */ +/* 104 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38126,9 +38294,9 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x4, - property = _x5, - receiver = _x6; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, + property = _x2, + receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } @@ -38136,252 +38304,201 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilBezierEdgeBase = __webpack_require__(108); + var _utilNodeBase = __webpack_require__(103); - var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var BezierEdgeStatic = (function (_BezierEdgeBase) { - function BezierEdgeStatic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeStatic); + var CircleImageBase = (function (_NodeBase) { + function CircleImageBase(options, body, labelModule) { + _classCallCheck(this, CircleImageBase); - _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(BezierEdgeStatic, _BezierEdgeBase); + _inherits(CircleImageBase, _NodeBase); - _createClass(BezierEdgeStatic, [{ - key: 'cleanup', - value: function cleanup() { - return false; + _createClass(CircleImageBase, [{ + key: '_resizeImage', + value: function _resizeImage() { + if (!this.width || !this.height) { + // undefined or 0 + var width, height, ratio; + if (this.imageObj.width && this.imageObj.height) { + // not undefined or 0 + width = 0; + height = 0; + } + if (this.imageObj.width > this.imageObj.height) { + ratio = this.imageObj.width / this.imageObj.height; + width = this.options.size * 2 * ratio || this.imageObj.width; + height = this.options.size * 2 || this.imageObj.height; + } else { + ratio = this.imageObj.height / this.imageObj.width; + width = this.options.size * 2 || this.imageObj.width; + height = this.options.size * 2 * ratio || this.imageObj.height; + } + this.width = width; + this.height = height; + } } }, { - key: '_line', + key: '_drawRawCircle', + value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - var via = this._getViaCoordinates(); - var returnValue = via; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(x, y, size); - // fallback to normal straight edges - if (via.x === undefined) { - ctx.lineTo(this.to.x, this.to.y); - returnValue = undefined; - } else { - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - } // draw shadow if enabled this.enableShadow(ctx); - ctx.stroke(); + ctx.fill(); + + // disable shadows for other elements. this.disableShadow(ctx); - return returnValue; + + ctx.stroke(); } }, { - key: '_getViaCoordinates', - value: function _getViaCoordinates() { - var xVia = undefined; - var yVia = undefined; - var factor = this.options.smooth.roundness; - var type = this.options.smooth.type; - var dx = Math.abs(this.from.x - this.to.x); - var dy = Math.abs(this.from.y - this.to.y); - if (type === 'discrete' || type === 'diagonalCross') { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - } - } - if (type === 'discrete') { - xVia = dx < factor * dy ? this.from.x : xVia; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - } - } - if (type === 'discrete') { - yVia = dy < factor * dx ? this.from.y : yVia; - } - } - } else if (type === 'straightCross') { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - // up - down - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - // left - right - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } - } else if (type === 'horizontal') { - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } else if (type === 'vertical') { - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; + key: '_drawImageAtPosition', + value: function _drawImageAtPosition(ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + + // disable shadows for other elements. + this.disableShadow(ctx); + } + } + }, { + key: '_drawImageLabel', + value: function _drawImageLabel(ctx, x, y, selected) { + var yLabel; + var offset = 0; + + if (this.height !== undefined) { + offset = this.height * 0.5; + var labelDimensions = this.labelModule.getTextSize(ctx); + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; } - } else if (type === 'curvedCW') { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + } + + yLabel = y + offset; + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + } + }]); + + return CircleImageBase; + })(_utilNodeBase2['default']); + + exports['default'] = CircleImageBase; + module.exports = exports['default']; + +/***/ }, +/* 105 */ +/***/ function(module, exports, __webpack_require__) { - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); + 'use strict'; - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } else if (type === 'curvedCCW') { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + Object.defineProperty(exports, '__esModule', { + value: true + }); - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + 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; }; })(); - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } else { - // continuous - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } - } - } + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, + property = _x2, + receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilNodeBase = __webpack_require__(103); + + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + + var ShapeBase = (function (_NodeBase) { + function ShapeBase(options, body, labelModule) { + _classCallCheck(this, ShapeBase); + + _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(ShapeBase, _NodeBase); + + _createClass(ShapeBase, [{ + key: '_resizeShape', + value: function _resizeShape() { + if (this.width === undefined) { + var size = 2 * this.options.size; + this.width = size; + this.height = size; } - return { x: xVia, y: yVia }; } }, { - key: '_findBorderPosition', - value: function _findBorderPosition(nearNode, ctx) { - var options = arguments[2] === undefined ? {} : arguments[2]; + key: '_drawShape', + value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { + this._resizeShape(); - return this._findBorderPositionBezier(nearNode, ctx, options.via); - } - }, { - key: '_getDistanceToEdge', - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); - } - }, { - key: 'getPoint', + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](x, y, this.options.size); - return { x: x, y: y }; + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + + ctx.stroke(); + + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + if (this.options.label !== undefined) { + var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + } } }]); - return BezierEdgeStatic; - })(_utilBezierEdgeBase2['default']); + return ShapeBase; + })(_utilNodeBase2['default']); - exports['default'] = BezierEdgeStatic; + exports['default'] = ShapeBase; module.exports = exports['default']; /***/ }, -/* 104 */ +/* 106 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38402,24 +38519,87 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilEdgeBase = __webpack_require__(109); + var _utilBezierEdgeBase = __webpack_require__(109); - var _utilEdgeBase2 = _interopRequireDefault(_utilEdgeBase); + var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); - var StraightEdge = (function (_EdgeBase) { - function StraightEdge(options, body, labelModule) { - _classCallCheck(this, StraightEdge); + var BezierEdgeDynamic = (function (_BezierEdgeBase) { + function BezierEdgeDynamic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeDynamic); - _get(Object.getPrototypeOf(StraightEdge.prototype), 'constructor', this).call(this, options, body, labelModule); + //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below } - _inherits(StraightEdge, _EdgeBase); + _inherits(BezierEdgeDynamic, _BezierEdgeBase); - _createClass(StraightEdge, [{ + _createClass(BezierEdgeDynamic, [{ + key: 'setOptions', + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + this.setupSupportNode(); + + // fix weird behaviour + if (this.from.id === this.to.id) { + this.via.setOptions({ physics: false }); + } else { + this.via.setOptions({ physics: true }); + } + } + }, { key: 'cleanup', value: function cleanup() { + if (this.via !== undefined) { + delete this.body.nodes[this.via.id]; + this.via = undefined; + return true; + } return false; } + }, { + key: 'togglePhysics', + value: function togglePhysics(status) { + this.via.setOptions({ physics: status }); + } + }, { + key: 'setupSupportNode', + + /** + * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but + * are used for the force calculation. + * + * The changed data is not called, if needed, it is returned by the main edge constructor. + * @private + */ + value: function setupSupportNode() { + if (this.via === undefined) { + var nodeId = 'edgeId:' + this.id; + var node = this.body.functions.createNode({ + id: nodeId, + shape: 'circle', + physics: true, + hidden: true + }); + this.body.nodes[nodeId] = node; + this.via = node; + this.via.parentEdgeId = this.id; + this.positionBezierNode(); + } + } + }, { + key: 'positionBezierNode', + value: function positionBezierNode() { + if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== undefined) { + this.via.x = 0; + this.via.y = 0; + } + } }, { key: '_line', @@ -38432,12 +38612,12 @@ return /******/ (function(modules) { // webpackBootstrap // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); // draw shadow if enabled this.enableShadow(ctx); ctx.stroke(); this.disableShadow(ctx); - return undefined; + return this.via; } }, { key: 'getPoint', @@ -38450,116 +38630,33 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ value: function getPoint(percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - }; + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + + return { x: x, y: y }; } }, { key: '_findBorderPosition', value: function _findBorderPosition(nearNode, ctx) { - var node1 = this.to; - var node2 = this.from; - if (nearNode.id === this.from.id) { - node1 = this.from; - node2 = this.to; - } - - var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - var dx = node1.x - node2.x; - var dy = node1.y - node2.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = nearNode.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - - var borderPos = {}; - borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; - borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; - - return borderPos; + return this._findBorderPositionBezier(nearNode, ctx, this.via); } }, { key: '_getDistanceToEdge', value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point - return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); - } - }]); - - return StraightEdge; - })(_utilEdgeBase2['default']); - - exports['default'] = StraightEdge; - module.exports = exports['default']; - -/***/ }, -/* 105 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - 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; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var NodeBase = (function () { - function NodeBase(options, body, labelModule) { - _classCallCheck(this, NodeBase); - - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.top = undefined; - this.left = undefined; - this.height = undefined; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; - } - - _createClass(NodeBase, [{ - key: 'setOptions', - value: function setOptions(options) { - this.options = options; - } - }, { - key: '_distanceToBorder', - value: function _distanceToBorder(angle) { - var borderWidth = 1; - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - } - }, { - key: 'enableShadow', - value: function enableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0.5)'; - ctx.shadowBlur = this.options.shadow.size; - ctx.shadowOffsetX = this.options.shadow.x; - ctx.shadowOffsetY = this.options.shadow.y; - } - } - }, { - key: 'disableShadow', - value: function disableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0)'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); } }]); - return NodeBase; - })(); + return BezierEdgeDynamic; + })(_utilBezierEdgeBase2['default']); - exports['default'] = NodeBase; + exports['default'] = BezierEdgeDynamic; module.exports = exports['default']; /***/ }, -/* 106 */ +/* 107 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38570,9 +38667,9 @@ return /******/ (function(modules) { // webpackBootstrap 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; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x, - property = _x2, - receiver = _x3; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { desc = parent = getter = undefined; _again = false; var object = _x4, + property = _x5, + receiver = _x6; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } @@ -38580,109 +38677,252 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilBezierEdgeBase = __webpack_require__(109); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); - var CircleImageBase = (function (_NodeBase) { - function CircleImageBase(options, body, labelModule) { - _classCallCheck(this, CircleImageBase); + var BezierEdgeStatic = (function (_BezierEdgeBase) { + function BezierEdgeStatic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeStatic); - _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(CircleImageBase, _NodeBase); + _inherits(BezierEdgeStatic, _BezierEdgeBase); - _createClass(CircleImageBase, [{ - key: '_resizeImage', - value: function _resizeImage() { - if (!this.width || !this.height) { - // undefined or 0 - var width, height, ratio; - if (this.imageObj.width && this.imageObj.height) { - // not undefined or 0 - width = 0; - height = 0; - } - if (this.imageObj.width > this.imageObj.height) { - ratio = this.imageObj.width / this.imageObj.height; - width = this.options.size * 2 * ratio || this.imageObj.width; - height = this.options.size * 2 || this.imageObj.height; - } else { - ratio = this.imageObj.height / this.imageObj.width; - width = this.options.size * 2 || this.imageObj.width; - height = this.options.size * 2 * ratio || this.imageObj.height; - } - this.width = width; - this.height = height; - } + _createClass(BezierEdgeStatic, [{ + key: 'cleanup', + value: function cleanup() { + return false; } }, { - key: '_drawRawCircle', - value: function _drawRawCircle(ctx, x, y, selected, hover, size) { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + key: '_line', - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + var via = this._getViaCoordinates(); + var returnValue = via; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + // fallback to normal straight edges + if (via.x === undefined) { + ctx.lineTo(this.to.x, this.to.y); + returnValue = undefined; + } else { + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + } + // draw shadow if enabled + this.enableShadow(ctx); + ctx.stroke(); + this.disableShadow(ctx); + return returnValue; + } + }, { + key: '_getViaCoordinates', + value: function _getViaCoordinates() { + var xVia = undefined; + var yVia = undefined; + var factor = this.options.smooth.roundness; + var type = this.options.smooth.type; + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type === 'discrete' || type === 'diagonalCross') { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type === 'discrete') { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type === 'discrete') { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } else if (type === 'straightCross') { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + // up - down + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + // left - right + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } + } else if (type === 'horizontal') { + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } else if (type === 'vertical') { + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (type === 'curvedCW') { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(x, y, size); + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else if (type === 'curvedCCW') { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - // disable shadows for other elements. - this.disableShadow(ctx); + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); - ctx.stroke(); + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else { + // continuous + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + } + } + } + return { x: xVia, y: yVia }; } }, { - key: '_drawImageAtPosition', - value: function _drawImageAtPosition(ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + key: '_findBorderPosition', + value: function _findBorderPosition(nearNode, ctx) { + var options = arguments[2] === undefined ? {} : arguments[2]; - // disable shadows for other elements. - this.disableShadow(ctx); - } + return this._findBorderPositionBezier(nearNode, ctx, options.via); } }, { - key: '_drawImageLabel', - value: function _drawImageLabel(ctx, x, y, selected) { - var yLabel; - var offset = 0; + key: '_getDistanceToEdge', + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); + } + }, { + key: 'getPoint', - if (this.height !== undefined) { - offset = this.height * 0.5; - var labelDimensions = this.labelModule.getTextSize(ctx); - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - } - } + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; - yLabel = y + offset; - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; + + return { x: x, y: y }; } }]); - return CircleImageBase; - })(_utilNodeBase2['default']); + return BezierEdgeStatic; + })(_utilBezierEdgeBase2['default']); - exports['default'] = CircleImageBase; + exports['default'] = BezierEdgeStatic; module.exports = exports['default']; /***/ }, -/* 107 */ +/* 108 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38703,78 +38943,98 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(105); + var _utilEdgeBase = __webpack_require__(110); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _utilEdgeBase2 = _interopRequireDefault(_utilEdgeBase); - var ShapeBase = (function (_NodeBase) { - function ShapeBase(options, body, labelModule) { - _classCallCheck(this, ShapeBase); + var StraightEdge = (function (_EdgeBase) { + function StraightEdge(options, body, labelModule) { + _classCallCheck(this, StraightEdge); - _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(StraightEdge.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(ShapeBase, _NodeBase); + _inherits(StraightEdge, _EdgeBase); - _createClass(ShapeBase, [{ - key: '_resizeShape', - value: function _resizeShape() { - if (this.width === undefined) { - var size = 2 * this.options.size; - this.width = size; - this.height = size; - } + _createClass(StraightEdge, [{ + key: 'cleanup', + value: function cleanup() { + return false; } }, { - key: '_drawShape', - value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { - this._resizeShape(); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](x, y, this.options.size); + key: '_line', + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); // draw shadow if enabled this.enableShadow(ctx); - ctx.fill(); - - // disable shadows for other elements. + ctx.stroke(); this.disableShadow(ctx); + return undefined; + } + }, { + key: 'getPoint', - ctx.stroke(); + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; + } + }, { + key: '_findBorderPosition', + value: function _findBorderPosition(nearNode, ctx) { + var node1 = this.to; + var node2 = this.from; + if (nearNode.id === this.from.id) { + node1 = this.from; + node2 = this.to; + } - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + var dx = node1.x - node2.x; + var dy = node1.y - node2.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = nearNode.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - if (this.options.label !== undefined) { - var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); - } + var borderPos = {}; + borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; + borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; + + return borderPos; + } + }, { + key: '_getDistanceToEdge', + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); } }]); - return ShapeBase; - })(_utilNodeBase2['default']); + return StraightEdge; + })(_utilEdgeBase2['default']); - exports['default'] = ShapeBase; + exports['default'] = StraightEdge; module.exports = exports['default']; /***/ }, -/* 108 */ +/* 109 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38795,7 +39055,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _EdgeBase2 = __webpack_require__(109); + var _EdgeBase2 = __webpack_require__(110); var _EdgeBase3 = _interopRequireDefault(_EdgeBase2); @@ -38923,7 +39183,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 109 */ +/* 110 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; diff --git a/docs/css/newdocs.css b/docs/css/newdocs.css index 747c6693..8c899fd8 100644 --- a/docs/css/newdocs.css +++ b/docs/css/newdocs.css @@ -182,7 +182,7 @@ td.default { } td.eventProperties { - width:350px; + width:150px; } td.methodName { diff --git a/docs/js/toggleTable.js b/docs/js/toggleTable.js index 88561038..ee46fbbf 100644 --- a/docs/js/toggleTable.js +++ b/docs/js/toggleTable.js @@ -57,22 +57,25 @@ function toggleTable(tableId, parent, clickedRow) { } } -function toggleTab(showTabId, showPreId, hideTabId, hidePreId) { +function toggleTab(showTabId, showDOMid, hideTabId, hideDOMid, hideTabId2, hideDOMid2) { + if (hideTabId2 !== undefined) { + document.getElementById(hideTabId2).className = ''; + document.getElementById(hideDOMid2).className = document.getElementById(hideDOMid2).className.replace(' hidden',''); + document.getElementById(hideDOMid2).className += ' hidden'; + } + if (hideTabId !== undefined) { document.getElementById(hideTabId).className = ''; - document.getElementById(hidePreId).className = document.getElementById(hidePreId).className.replace(' hidden',''); - document.getElementById(hidePreId).className += ' hidden'; + document.getElementById(hideDOMid).className = document.getElementById(hideDOMid).className.replace(' hidden',''); + document.getElementById(hideDOMid).className += ' hidden'; } - document.getElementById('hiddenTab').className = ''; document.getElementById(showTabId).className = 'active'; - document.getElementById(showPreId).className = document.getElementById(showPreId).className.replace(' hidden',''); + document.getElementById(showDOMid).className = document.getElementById(showDOMid).className.replace(' hidden',''); } function hideOptions(hideTabId1, hidePreId1, hideTabId2, hidePreId2) { - document.getElementById('hiddenTab').className = 'active'; - document.getElementById(hideTabId1).className = ''; document.getElementById(hidePreId1).className = document.getElementById(hidePreId1).className.replace(' hidden',''); document.getElementById(hidePreId1).className += ' hidden'; diff --git a/docs/network/edges.html b/docs/network/edges.html index 3185e017..44be6b84 100644 --- a/docs/network/edges.html +++ b/docs/network/edges.html @@ -121,7 +121,7 @@ var options = { size: 14, // px face: 'arial', background: 'none', - strokeWidth: 1, // px + strokeWidth: 2, // px strokeColor: '#ffffff', align:'horizontal' }, @@ -344,7 +344,7 @@ network.setOptions(options); font.strokeWidth Number - 0 + 2 As an alternative to the background rectangle, a stroke can be drawn around the text. When a value higher than 0 is supplied, the stroke will be drawn. diff --git a/docs/network/new_network.html b/docs/network/new_network.html index d37bb65b..6a4f8919 100644 --- a/docs/network/new_network.html +++ b/docs/network/new_network.html @@ -30,20 +30,6 @@ @@ -114,7 +99,11 @@

Network

+

Network is a visualization to display networks and networks consisting of nodes and edges. The visualization + is easy to use and supports custom shapes, styles, colors, sizes, images, and more. + The network visualization works smooth on any modern browser for up to a few thousand nodes and edges. To + handle a larger amount of nodes, Network has clustering support. Network uses HTML canvas for rendering.

As of 4.0, the network consists of individual modules which handle specific parts of the network. These modules have their own docs, options, methods and events which you can access by clicking on the modules in the list below.

@@ -161,10 +150,23 @@ -

Options

+
+
-

Click on the options shown to show how these options are supposed to be used.

+
+ +
 var options = {
   autoResize: true,
@@ -185,325 +187,364 @@ var options = {
 
 network.setOptions(options);
 
-

The individual options are explained below. The ones referring to modules are explained in the corresponding - module.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
autoResizeBooleantrueIf true, the Network will automatically detect when its container is resized, and redraw itself - accordingly. If false, the Network can be forced to repaint after its container has been resized - using the function redraw() and setSize(). -
widthString'100%'the width of the canvas. Can be in percentages or pixels (ie. '400px').
heightString'100%'the height of the canvas. Can be in percentages or pixels (ie. '400px').
localeString'en'Select the locale. By default, the language is English. If you want to use another language, you will - need to define your own locale and refer to it here. -
localesObjectdefaultLocalesLocales object. By default only 'en' and 'nl' are supported. Take a look at - the locales - section below for more explaination on how to customize this. -
clickToUseBooleanfalseLocales object. By default only 'en' and 'nl' are supported. Take a look at - the locales - section below for more explaination on how to customize this. -
- - -
-
- - -

All Methods

- -

This is a list of all the methods in the public API. They have been grouped by category, which correspond to the - modules listed above.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Global methods for the network.
destroy()
setData({nodes: vis - DataSet/Array,edges: vis - DataSet/Array}) -
setOptions(Object options) -
Methods related to the canvas.
canvasToDOM({x: - Number,y: - Number}) -
DOMtoCanvas({x: - Number,y: - Number}) -
redraw()
setSize(String width,String - height) -
Clustering
cluster( - Object options) -
clusterByConnection( - String nodeId, - [Object options] - ) -
clusterByHubsize( - Number hubsize, - [Object options]) -
clusterOutliers( - [Object options]) -
findNode( - String nodeId) -
isCluster( - String nodeId) -
openCluster( - String nodeId) -
Layout
getSeed()
Manipulation methods to use the manipulation system without GUI.
enableEditMode()
disableEditMode()
addNodeMode()
editNodeMode()
addEdgeMode()
editEdgeMode()
deleteSelected()
Methods to get information on nodes.
getPositions([Array of - nodeIds]) -
Events triggered by the rendering module. Can be used to draw custom elements on the - canvas. -
initRedrawnoneFired before the redrawing begins. The simulation step has completed at this point. Can be used to move - custom elements before starting drawing the new frame. -
beforeDrawingcanvas contextFired after the canvas has been cleared, scaled and translated to the viewing position but before all - edges and nodes are drawn. Can be used to draw behind the network. -
afterDrawingcanvas contextFired after drawing on the canvas has been completed. Can be used to draw on top of the network.
Event triggered by the view module.
animationFinishednoneFired when an animation is finished.
+

diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index d9a442c4..8042e2c2 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -34,7 +34,6 @@ // create an array with edges var edges = new vis.DataSet([ - {from: 1, to: 1}, {from: 1, to: 3}, {from: 1, to: 2}, {from: 2, to: 4}, @@ -47,7 +46,7 @@ nodes: nodes, edges: edges }; - var options = {loale:'en'}; + var options = {locale:'en'}; var network = new vis.Network(container, data, options); diff --git a/lib/network/dotparser.js b/lib/network/dotparser.js index c6145c32..dd0c73f7 100644 --- a/lib/network/dotparser.js +++ b/lib/network/dotparser.js @@ -767,7 +767,8 @@ function DOTToGraph (data) { to: dotEdge.to }; merge(graphEdge, dotEdge.attr); - graphEdge.style = (dotEdge.type === '->') ? 'arrow' : 'line'; + graphEdge.arrows = (dotEdge.type === '->') ? 'to' : undefined; + return graphEdge; }; @@ -822,5 +823,5 @@ function DOTToGraph (data) { } // exports -exports.parseDOT = parseDOT; +exports.parseDOT = parseDOT; exports.DOTToGraph = DOTToGraph; diff --git a/lib/network/gephiParser.js b/lib/network/gephiParser.js index 97be0d1a..e0c508f3 100644 --- a/lib/network/gephiParser.js +++ b/lib/network/gephiParser.js @@ -1,21 +1,21 @@ -function parseGephi(gephiJSON, options) { +function parseGephi(gephiJSON, optionsObj) { var edges = []; var nodes = []; - this.options = { + var options = { edges: { inheritColor: true }, nodes: { - allowedToMove: false, + fixed: false, parseColor: false } }; if (options !== undefined) { - this.options.nodes['allowedToMove'] = options.allowedToMove | false; - this.options.nodes['parseColor'] = options.parseColor | false; - this.options.edges['inheritColor'] = options.inheritColor | true; + options.nodes['fixed'] = optionsObj.fixed !== undefined ? options.fixed : false; + options.nodes['parseColor'] = optionsObj.parseColor !== undefined ? options.parseColor : false; + options.edges['inheritColor'] = optionsObj.inheritColor !== undefined ? options.inheritColor : true; } var gEdges = gephiJSON.edges; @@ -30,7 +30,7 @@ function parseGephi(gephiJSON, options) { // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; edge['color'] = gEdge.color; - edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor; + edge['inheritColor'] = edge['color'] !== undefined ? false : options.inheritColor; edges.push(edge); } @@ -39,18 +39,18 @@ function parseGephi(gephiJSON, options) { var gNode = gNodes[i]; node['id'] = gNode.id; node['attributes'] = gNode.attributes; + node['title'] = gNode.title; node['x'] = gNode.x; node['y'] = gNode.y; node['label'] = gNode.label; - if (this.options.nodes.parseColor === true) { + if (options.nodes.parseColor === true) { node['color'] = gNode.color; } else { node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined; } - node['radius'] = gNode.size; - node['allowedToMoveX'] = this.options.nodes.allowedToMove; - node['allowedToMoveY'] = this.options.nodes.allowedToMove; + node['size'] = gNode.size; + node['fixed'] = options.nodes.fixed && gNode.x !== undefined && gNode.y !== undefined; nodes.push(node); } diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index 47f30714..fbd4ba92 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -40,7 +40,7 @@ class EdgesHandler { size: 14, // px face: 'arial', background: 'none', - strokeWidth: 0, // px + strokeWidth: 2, // px strokeColor: '#ffffff', align:'horizontal' }, diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index a4d38393..568fa76f 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -396,16 +396,15 @@ class Edge { else { var x, y; var radius = this.options.selfReferenceSize; - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; + if (node1.shape.width > node1.shape.height) { + x = node1.x + node1.shape.width * 0.5; y = node1.y - radius; } else { x = node1.x + radius; - y = node1.y - node1.height * 0.5; + y = node1.y - node1.shape.height * 0.5; } point = this._pointOnCircle(x, y, radius, 0.125); - this.labelModule.draw(ctx, point.x, point.y, selected); } }