vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15714 lines
471 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.3.0-SNAPSHOT
  8. * @date 2013-10-30
  9. *
  10. * @license
  11. * Copyright (C) 2011-2013 Almende B.V, http://almende.com
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. !function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.vis=e():"undefined"!=typeof global?global.vis=e():"undefined"!=typeof self&&(self.vis=e())}(function(){var define,module,exports;
  26. return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  27. /*! Hammer.JS - v1.0.5 - 2013-04-07
  28. * http://eightmedia.github.com/hammer.js
  29. *
  30. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  31. * Licensed under the MIT license */
  32. (function(window, undefined) {
  33. 'use strict';
  34. /**
  35. * Hammer
  36. * use this to create instances
  37. * @param {HTMLElement} element
  38. * @param {Object} options
  39. * @returns {Hammer.Instance}
  40. * @constructor
  41. */
  42. var Hammer = function(element, options) {
  43. return new Hammer.Instance(element, options || {});
  44. };
  45. // default settings
  46. Hammer.defaults = {
  47. // add styles and attributes to the element to prevent the browser from doing
  48. // its native behavior. this doesnt prevent the scrolling, but cancels
  49. // the contextmenu, tap highlighting etc
  50. // set to false to disable this
  51. stop_browser_behavior: {
  52. // this also triggers onselectstart=false for IE
  53. userSelect: 'none',
  54. // this makes the element blocking in IE10 >, you could experiment with the value
  55. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  56. touchAction: 'none',
  57. touchCallout: 'none',
  58. contentZooming: 'none',
  59. userDrag: 'none',
  60. tapHighlightColor: 'rgba(0,0,0,0)'
  61. }
  62. // more settings are defined per gesture at gestures.js
  63. };
  64. // detect touchevents
  65. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  66. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  67. // dont use mouseevents on mobile devices
  68. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  69. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  70. // eventtypes per touchevent (start, move, end)
  71. // are filled by Hammer.event.determineEventTypes on setup
  72. Hammer.EVENT_TYPES = {};
  73. // direction defines
  74. Hammer.DIRECTION_DOWN = 'down';
  75. Hammer.DIRECTION_LEFT = 'left';
  76. Hammer.DIRECTION_UP = 'up';
  77. Hammer.DIRECTION_RIGHT = 'right';
  78. // pointer type
  79. Hammer.POINTER_MOUSE = 'mouse';
  80. Hammer.POINTER_TOUCH = 'touch';
  81. Hammer.POINTER_PEN = 'pen';
  82. // touch event defines
  83. Hammer.EVENT_START = 'start';
  84. Hammer.EVENT_MOVE = 'move';
  85. Hammer.EVENT_END = 'end';
  86. // hammer document where the base events are added at
  87. Hammer.DOCUMENT = document;
  88. // plugins namespace
  89. Hammer.plugins = {};
  90. // if the window events are set...
  91. Hammer.READY = false;
  92. /**
  93. * setup events to detect gestures on the document
  94. */
  95. function setup() {
  96. if(Hammer.READY) {
  97. return;
  98. }
  99. // find what eventtypes we add listeners to
  100. Hammer.event.determineEventTypes();
  101. // Register all gestures inside Hammer.gestures
  102. for(var name in Hammer.gestures) {
  103. if(Hammer.gestures.hasOwnProperty(name)) {
  104. Hammer.detection.register(Hammer.gestures[name]);
  105. }
  106. }
  107. // Add touch events on the document
  108. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  109. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  110. // Hammer is ready...!
  111. Hammer.READY = true;
  112. }
  113. /**
  114. * create new hammer instance
  115. * all methods should return the instance itself, so it is chainable.
  116. * @param {HTMLElement} element
  117. * @param {Object} [options={}]
  118. * @returns {Hammer.Instance}
  119. * @constructor
  120. */
  121. Hammer.Instance = function(element, options) {
  122. var self = this;
  123. // setup HammerJS window events and register all gestures
  124. // this also sets up the default options
  125. setup();
  126. this.element = element;
  127. // start/stop detection option
  128. this.enabled = true;
  129. // merge options
  130. this.options = Hammer.utils.extend(
  131. Hammer.utils.extend({}, Hammer.defaults),
  132. options || {});
  133. // add some css to the element to prevent the browser from doing its native behavoir
  134. if(this.options.stop_browser_behavior) {
  135. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  136. }
  137. // start detection on touchstart
  138. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  139. if(self.enabled) {
  140. Hammer.detection.startDetect(self, ev);
  141. }
  142. });
  143. // return instance
  144. return this;
  145. };
  146. Hammer.Instance.prototype = {
  147. /**
  148. * bind events to the instance
  149. * @param {String} gesture
  150. * @param {Function} handler
  151. * @returns {Hammer.Instance}
  152. */
  153. on: function onEvent(gesture, handler){
  154. var gestures = gesture.split(' ');
  155. for(var t=0; t<gestures.length; t++) {
  156. this.element.addEventListener(gestures[t], handler, false);
  157. }
  158. return this;
  159. },
  160. /**
  161. * unbind events to the instance
  162. * @param {String} gesture
  163. * @param {Function} handler
  164. * @returns {Hammer.Instance}
  165. */
  166. off: function offEvent(gesture, handler){
  167. var gestures = gesture.split(' ');
  168. for(var t=0; t<gestures.length; t++) {
  169. this.element.removeEventListener(gestures[t], handler, false);
  170. }
  171. return this;
  172. },
  173. /**
  174. * trigger gesture event
  175. * @param {String} gesture
  176. * @param {Object} eventData
  177. * @returns {Hammer.Instance}
  178. */
  179. trigger: function triggerEvent(gesture, eventData){
  180. // create DOM event
  181. var event = Hammer.DOCUMENT.createEvent('Event');
  182. event.initEvent(gesture, true, true);
  183. event.gesture = eventData;
  184. // trigger on the target if it is in the instance element,
  185. // this is for event delegation tricks
  186. var element = this.element;
  187. if(Hammer.utils.hasParent(eventData.target, element)) {
  188. element = eventData.target;
  189. }
  190. element.dispatchEvent(event);
  191. return this;
  192. },
  193. /**
  194. * enable of disable hammer.js detection
  195. * @param {Boolean} state
  196. * @returns {Hammer.Instance}
  197. */
  198. enable: function enable(state) {
  199. this.enabled = state;
  200. return this;
  201. }
  202. };
  203. /**
  204. * this holds the last move event,
  205. * used to fix empty touchend issue
  206. * see the onTouch event for an explanation
  207. * @type {Object}
  208. */
  209. var last_move_event = null;
  210. /**
  211. * when the mouse is hold down, this is true
  212. * @type {Boolean}
  213. */
  214. var enable_detect = false;
  215. /**
  216. * when touch events have been fired, this is true
  217. * @type {Boolean}
  218. */
  219. var touch_triggered = false;
  220. Hammer.event = {
  221. /**
  222. * simple addEventListener
  223. * @param {HTMLElement} element
  224. * @param {String} type
  225. * @param {Function} handler
  226. */
  227. bindDom: function(element, type, handler) {
  228. var types = type.split(' ');
  229. for(var t=0; t<types.length; t++) {
  230. element.addEventListener(types[t], handler, false);
  231. }
  232. },
  233. /**
  234. * touch events with mouse fallback
  235. * @param {HTMLElement} element
  236. * @param {String} eventType like Hammer.EVENT_MOVE
  237. * @param {Function} handler
  238. */
  239. onTouch: function onTouch(element, eventType, handler) {
  240. var self = this;
  241. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  242. var sourceEventType = ev.type.toLowerCase();
  243. // onmouseup, but when touchend has been fired we do nothing.
  244. // this is for touchdevices which also fire a mouseup on touchend
  245. if(sourceEventType.match(/mouse/) && touch_triggered) {
  246. return;
  247. }
  248. // mousebutton must be down or a touch event
  249. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  250. sourceEventType.match(/pointerdown/) || // pointerevents touch
  251. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  252. ){
  253. enable_detect = true;
  254. }
  255. // we are in a touch event, set the touch triggered bool to true,
  256. // this for the conflicts that may occur on ios and android
  257. if(sourceEventType.match(/touch|pointer/)) {
  258. touch_triggered = true;
  259. }
  260. // count the total touches on the screen
  261. var count_touches = 0;
  262. // when touch has been triggered in this detection session
  263. // and we are now handling a mouse event, we stop that to prevent conflicts
  264. if(enable_detect) {
  265. // update pointerevent
  266. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  267. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  268. }
  269. // touch
  270. else if(sourceEventType.match(/touch/)) {
  271. count_touches = ev.touches.length;
  272. }
  273. // mouse
  274. else if(!touch_triggered) {
  275. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  276. }
  277. // if we are in a end event, but when we remove one touch and
  278. // we still have enough, set eventType to move
  279. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  280. eventType = Hammer.EVENT_MOVE;
  281. }
  282. // no touches, force the end event
  283. else if(!count_touches) {
  284. eventType = Hammer.EVENT_END;
  285. }
  286. // because touchend has no touches, and we often want to use these in our gestures,
  287. // we send the last move event as our eventData in touchend
  288. if(!count_touches && last_move_event !== null) {
  289. ev = last_move_event;
  290. }
  291. // store the last move event
  292. else {
  293. last_move_event = ev;
  294. }
  295. // trigger the handler
  296. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  297. // remove pointerevent from list
  298. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  299. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  300. }
  301. }
  302. //debug(sourceEventType +" "+ eventType);
  303. // on the end we reset everything
  304. if(!count_touches) {
  305. last_move_event = null;
  306. enable_detect = false;
  307. touch_triggered = false;
  308. Hammer.PointerEvent.reset();
  309. }
  310. });
  311. },
  312. /**
  313. * we have different events for each device/browser
  314. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  315. */
  316. determineEventTypes: function determineEventTypes() {
  317. // determine the eventtype we want to set
  318. var types;
  319. // pointerEvents magic
  320. if(Hammer.HAS_POINTEREVENTS) {
  321. types = Hammer.PointerEvent.getEvents();
  322. }
  323. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  324. else if(Hammer.NO_MOUSEEVENTS) {
  325. types = [
  326. 'touchstart',
  327. 'touchmove',
  328. 'touchend touchcancel'];
  329. }
  330. // for non pointer events browsers and mixed browsers,
  331. // like chrome on windows8 touch laptop
  332. else {
  333. types = [
  334. 'touchstart mousedown',
  335. 'touchmove mousemove',
  336. 'touchend touchcancel mouseup'];
  337. }
  338. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  339. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  340. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  341. },
  342. /**
  343. * create touchlist depending on the event
  344. * @param {Object} ev
  345. * @param {String} eventType used by the fakemultitouch plugin
  346. */
  347. getTouchList: function getTouchList(ev/*, eventType*/) {
  348. // get the fake pointerEvent touchlist
  349. if(Hammer.HAS_POINTEREVENTS) {
  350. return Hammer.PointerEvent.getTouchList();
  351. }
  352. // get the touchlist
  353. else if(ev.touches) {
  354. return ev.touches;
  355. }
  356. // make fake touchlist from mouse position
  357. else {
  358. return [{
  359. identifier: 1,
  360. pageX: ev.pageX,
  361. pageY: ev.pageY,
  362. target: ev.target
  363. }];
  364. }
  365. },
  366. /**
  367. * collect event data for Hammer js
  368. * @param {HTMLElement} element
  369. * @param {String} eventType like Hammer.EVENT_MOVE
  370. * @param {Object} eventData
  371. */
  372. collectEventData: function collectEventData(element, eventType, ev) {
  373. var touches = this.getTouchList(ev, eventType);
  374. // find out pointerType
  375. var pointerType = Hammer.POINTER_TOUCH;
  376. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  377. pointerType = Hammer.POINTER_MOUSE;
  378. }
  379. return {
  380. center : Hammer.utils.getCenter(touches),
  381. timeStamp : new Date().getTime(),
  382. target : ev.target,
  383. touches : touches,
  384. eventType : eventType,
  385. pointerType : pointerType,
  386. srcEvent : ev,
  387. /**
  388. * prevent the browser default actions
  389. * mostly used to disable scrolling of the browser
  390. */
  391. preventDefault: function() {
  392. if(this.srcEvent.preventManipulation) {
  393. this.srcEvent.preventManipulation();
  394. }
  395. if(this.srcEvent.preventDefault) {
  396. this.srcEvent.preventDefault();
  397. }
  398. },
  399. /**
  400. * stop bubbling the event up to its parents
  401. */
  402. stopPropagation: function() {
  403. this.srcEvent.stopPropagation();
  404. },
  405. /**
  406. * immediately stop gesture detection
  407. * might be useful after a swipe was detected
  408. * @return {*}
  409. */
  410. stopDetect: function() {
  411. return Hammer.detection.stopDetect();
  412. }
  413. };
  414. }
  415. };
  416. Hammer.PointerEvent = {
  417. /**
  418. * holds all pointers
  419. * @type {Object}
  420. */
  421. pointers: {},
  422. /**
  423. * get a list of pointers
  424. * @returns {Array} touchlist
  425. */
  426. getTouchList: function() {
  427. var self = this;
  428. var touchlist = [];
  429. // we can use forEach since pointerEvents only is in IE10
  430. Object.keys(self.pointers).sort().forEach(function(id) {
  431. touchlist.push(self.pointers[id]);
  432. });
  433. return touchlist;
  434. },
  435. /**
  436. * update the position of a pointer
  437. * @param {String} type Hammer.EVENT_END
  438. * @param {Object} pointerEvent
  439. */
  440. updatePointer: function(type, pointerEvent) {
  441. if(type == Hammer.EVENT_END) {
  442. this.pointers = {};
  443. }
  444. else {
  445. pointerEvent.identifier = pointerEvent.pointerId;
  446. this.pointers[pointerEvent.pointerId] = pointerEvent;
  447. }
  448. return Object.keys(this.pointers).length;
  449. },
  450. /**
  451. * check if ev matches pointertype
  452. * @param {String} pointerType Hammer.POINTER_MOUSE
  453. * @param {PointerEvent} ev
  454. */
  455. matchType: function(pointerType, ev) {
  456. if(!ev.pointerType) {
  457. return false;
  458. }
  459. var types = {};
  460. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  461. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  462. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  463. return types[pointerType];
  464. },
  465. /**
  466. * get events
  467. */
  468. getEvents: function() {
  469. return [
  470. 'pointerdown MSPointerDown',
  471. 'pointermove MSPointerMove',
  472. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  473. ];
  474. },
  475. /**
  476. * reset the list
  477. */
  478. reset: function() {
  479. this.pointers = {};
  480. }
  481. };
  482. Hammer.utils = {
  483. /**
  484. * extend method,
  485. * also used for cloning when dest is an empty object
  486. * @param {Object} dest
  487. * @param {Object} src
  488. * @parm {Boolean} merge do a merge
  489. * @returns {Object} dest
  490. */
  491. extend: function extend(dest, src, merge) {
  492. for (var key in src) {
  493. if(dest[key] !== undefined && merge) {
  494. continue;
  495. }
  496. dest[key] = src[key];
  497. }
  498. return dest;
  499. },
  500. /**
  501. * find if a node is in the given parent
  502. * used for event delegation tricks
  503. * @param {HTMLElement} node
  504. * @param {HTMLElement} parent
  505. * @returns {boolean} has_parent
  506. */
  507. hasParent: function(node, parent) {
  508. while(node){
  509. if(node == parent) {
  510. return true;
  511. }
  512. node = node.parentNode;
  513. }
  514. return false;
  515. },
  516. /**
  517. * get the center of all the touches
  518. * @param {Array} touches
  519. * @returns {Object} center
  520. */
  521. getCenter: function getCenter(touches) {
  522. var valuesX = [], valuesY = [];
  523. for(var t= 0,len=touches.length; t<len; t++) {
  524. valuesX.push(touches[t].pageX);
  525. valuesY.push(touches[t].pageY);
  526. }
  527. return {
  528. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  529. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  530. };
  531. },
  532. /**
  533. * calculate the velocity between two points
  534. * @param {Number} delta_time
  535. * @param {Number} delta_x
  536. * @param {Number} delta_y
  537. * @returns {Object} velocity
  538. */
  539. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  540. return {
  541. x: Math.abs(delta_x / delta_time) || 0,
  542. y: Math.abs(delta_y / delta_time) || 0
  543. };
  544. },
  545. /**
  546. * calculate the angle between two coordinates
  547. * @param {Touch} touch1
  548. * @param {Touch} touch2
  549. * @returns {Number} angle
  550. */
  551. getAngle: function getAngle(touch1, touch2) {
  552. var y = touch2.pageY - touch1.pageY,
  553. x = touch2.pageX - touch1.pageX;
  554. return Math.atan2(y, x) * 180 / Math.PI;
  555. },
  556. /**
  557. * angle to direction define
  558. * @param {Touch} touch1
  559. * @param {Touch} touch2
  560. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  561. */
  562. getDirection: function getDirection(touch1, touch2) {
  563. var x = Math.abs(touch1.pageX - touch2.pageX),
  564. y = Math.abs(touch1.pageY - touch2.pageY);
  565. if(x >= y) {
  566. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  567. }
  568. else {
  569. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  570. }
  571. },
  572. /**
  573. * calculate the distance between two touches
  574. * @param {Touch} touch1
  575. * @param {Touch} touch2
  576. * @returns {Number} distance
  577. */
  578. getDistance: function getDistance(touch1, touch2) {
  579. var x = touch2.pageX - touch1.pageX,
  580. y = touch2.pageY - touch1.pageY;
  581. return Math.sqrt((x*x) + (y*y));
  582. },
  583. /**
  584. * calculate the scale factor between two touchLists (fingers)
  585. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  586. * @param {Array} start
  587. * @param {Array} end
  588. * @returns {Number} scale
  589. */
  590. getScale: function getScale(start, end) {
  591. // need two fingers...
  592. if(start.length >= 2 && end.length >= 2) {
  593. return this.getDistance(end[0], end[1]) /
  594. this.getDistance(start[0], start[1]);
  595. }
  596. return 1;
  597. },
  598. /**
  599. * calculate the rotation degrees between two touchLists (fingers)
  600. * @param {Array} start
  601. * @param {Array} end
  602. * @returns {Number} rotation
  603. */
  604. getRotation: function getRotation(start, end) {
  605. // need two fingers
  606. if(start.length >= 2 && end.length >= 2) {
  607. return this.getAngle(end[1], end[0]) -
  608. this.getAngle(start[1], start[0]);
  609. }
  610. return 0;
  611. },
  612. /**
  613. * boolean if the direction is vertical
  614. * @param {String} direction
  615. * @returns {Boolean} is_vertical
  616. */
  617. isVertical: function isVertical(direction) {
  618. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  619. },
  620. /**
  621. * stop browser default behavior with css props
  622. * @param {HtmlElement} element
  623. * @param {Object} css_props
  624. */
  625. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  626. var prop,
  627. vendors = ['webkit','khtml','moz','ms','o',''];
  628. if(!css_props || !element.style) {
  629. return;
  630. }
  631. // with css properties for modern browsers
  632. for(var i = 0; i < vendors.length; i++) {
  633. for(var p in css_props) {
  634. if(css_props.hasOwnProperty(p)) {
  635. prop = p;
  636. // vender prefix at the property
  637. if(vendors[i]) {
  638. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  639. }
  640. // set the style
  641. element.style[prop] = css_props[p];
  642. }
  643. }
  644. }
  645. // also the disable onselectstart
  646. if(css_props.userSelect == 'none') {
  647. element.onselectstart = function() {
  648. return false;
  649. };
  650. }
  651. }
  652. };
  653. Hammer.detection = {
  654. // contains all registred Hammer.gestures in the correct order
  655. gestures: [],
  656. // data of the current Hammer.gesture detection session
  657. current: null,
  658. // the previous Hammer.gesture session data
  659. // is a full clone of the previous gesture.current object
  660. previous: null,
  661. // when this becomes true, no gestures are fired
  662. stopped: false,
  663. /**
  664. * start Hammer.gesture detection
  665. * @param {Hammer.Instance} inst
  666. * @param {Object} eventData
  667. */
  668. startDetect: function startDetect(inst, eventData) {
  669. // already busy with a Hammer.gesture detection on an element
  670. if(this.current) {
  671. return;
  672. }
  673. this.stopped = false;
  674. this.current = {
  675. inst : inst, // reference to HammerInstance we're working for
  676. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  677. lastEvent : false, // last eventData
  678. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  679. };
  680. this.detect(eventData);
  681. },
  682. /**
  683. * Hammer.gesture detection
  684. * @param {Object} eventData
  685. * @param {Object} eventData
  686. */
  687. detect: function detect(eventData) {
  688. if(!this.current || this.stopped) {
  689. return;
  690. }
  691. // extend event data with calculations about scale, distance etc
  692. eventData = this.extendEventData(eventData);
  693. // instance options
  694. var inst_options = this.current.inst.options;
  695. // call Hammer.gesture handlers
  696. for(var g=0,len=this.gestures.length; g<len; g++) {
  697. var gesture = this.gestures[g];
  698. // only when the instance options have enabled this gesture
  699. if(!this.stopped && inst_options[gesture.name] !== false) {
  700. // if a handler returns false, we stop with the detection
  701. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  702. this.stopDetect();
  703. break;
  704. }
  705. }
  706. }
  707. // store as previous event event
  708. if(this.current) {
  709. this.current.lastEvent = eventData;
  710. }
  711. // endevent, but not the last touch, so dont stop
  712. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  713. this.stopDetect();
  714. }
  715. return eventData;
  716. },
  717. /**
  718. * clear the Hammer.gesture vars
  719. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  720. * to stop other Hammer.gestures from being fired
  721. */
  722. stopDetect: function stopDetect() {
  723. // clone current data to the store as the previous gesture
  724. // used for the double tap gesture, since this is an other gesture detect session
  725. this.previous = Hammer.utils.extend({}, this.current);
  726. // reset the current
  727. this.current = null;
  728. // stopped!
  729. this.stopped = true;
  730. },
  731. /**
  732. * extend eventData for Hammer.gestures
  733. * @param {Object} ev
  734. * @returns {Object} ev
  735. */
  736. extendEventData: function extendEventData(ev) {
  737. var startEv = this.current.startEvent;
  738. // if the touches change, set the new touches over the startEvent touches
  739. // this because touchevents don't have all the touches on touchstart, or the
  740. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  741. // but, sometimes it happens that both fingers are touching at the EXACT same time
  742. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  743. // extend 1 level deep to get the touchlist with the touch objects
  744. startEv.touches = [];
  745. for(var i=0,len=ev.touches.length; i<len; i++) {
  746. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  747. }
  748. }
  749. var delta_time = ev.timeStamp - startEv.timeStamp,
  750. delta_x = ev.center.pageX - startEv.center.pageX,
  751. delta_y = ev.center.pageY - startEv.center.pageY,
  752. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  753. Hammer.utils.extend(ev, {
  754. deltaTime : delta_time,
  755. deltaX : delta_x,
  756. deltaY : delta_y,
  757. velocityX : velocity.x,
  758. velocityY : velocity.y,
  759. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  760. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  761. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  762. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  763. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  764. startEvent : startEv
  765. });
  766. return ev;
  767. },
  768. /**
  769. * register new gesture
  770. * @param {Object} gesture object, see gestures.js for documentation
  771. * @returns {Array} gestures
  772. */
  773. register: function register(gesture) {
  774. // add an enable gesture options if there is no given
  775. var options = gesture.defaults || {};
  776. if(options[gesture.name] === undefined) {
  777. options[gesture.name] = true;
  778. }
  779. // extend Hammer default options with the Hammer.gesture options
  780. Hammer.utils.extend(Hammer.defaults, options, true);
  781. // set its index
  782. gesture.index = gesture.index || 1000;
  783. // add Hammer.gesture to the list
  784. this.gestures.push(gesture);
  785. // sort the list by index
  786. this.gestures.sort(function(a, b) {
  787. if (a.index < b.index) {
  788. return -1;
  789. }
  790. if (a.index > b.index) {
  791. return 1;
  792. }
  793. return 0;
  794. });
  795. return this.gestures;
  796. }
  797. };
  798. Hammer.gestures = Hammer.gestures || {};
  799. /**
  800. * Custom gestures
  801. * ==============================
  802. *
  803. * Gesture object
  804. * --------------------
  805. * The object structure of a gesture:
  806. *
  807. * { name: 'mygesture',
  808. * index: 1337,
  809. * defaults: {
  810. * mygesture_option: true
  811. * }
  812. * handler: function(type, ev, inst) {
  813. * // trigger gesture event
  814. * inst.trigger(this.name, ev);
  815. * }
  816. * }
  817. * @param {String} name
  818. * this should be the name of the gesture, lowercase
  819. * it is also being used to disable/enable the gesture per instance config.
  820. *
  821. * @param {Number} [index=1000]
  822. * the index of the gesture, where it is going to be in the stack of gestures detection
  823. * like when you build an gesture that depends on the drag gesture, it is a good
  824. * idea to place it after the index of the drag gesture.
  825. *
  826. * @param {Object} [defaults={}]
  827. * the default settings of the gesture. these are added to the instance settings,
  828. * and can be overruled per instance. you can also add the name of the gesture,
  829. * but this is also added by default (and set to true).
  830. *
  831. * @param {Function} handler
  832. * this handles the gesture detection of your custom gesture and receives the
  833. * following arguments:
  834. *
  835. * @param {Object} eventData
  836. * event data containing the following properties:
  837. * timeStamp {Number} time the event occurred
  838. * target {HTMLElement} target element
  839. * touches {Array} touches (fingers, pointers, mouse) on the screen
  840. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  841. * center {Object} center position of the touches. contains pageX and pageY
  842. * deltaTime {Number} the total time of the touches in the screen
  843. * deltaX {Number} the delta on x axis we haved moved
  844. * deltaY {Number} the delta on y axis we haved moved
  845. * velocityX {Number} the velocity on the x
  846. * velocityY {Number} the velocity on y
  847. * angle {Number} the angle we are moving
  848. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  849. * distance {Number} the distance we haved moved
  850. * scale {Number} scaling of the touches, needs 2 touches
  851. * rotation {Number} rotation of the touches, needs 2 touches *
  852. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  853. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  854. * startEvent {Object} contains the same properties as above,
  855. * but from the first touch. this is used to calculate
  856. * distances, deltaTime, scaling etc
  857. *
  858. * @param {Hammer.Instance} inst
  859. * the instance we are doing the detection for. you can get the options from
  860. * the inst.options object and trigger the gesture event by calling inst.trigger
  861. *
  862. *
  863. * Handle gestures
  864. * --------------------
  865. * inside the handler you can get/set Hammer.detection.current. This is the current
  866. * detection session. It has the following properties
  867. * @param {String} name
  868. * contains the name of the gesture we have detected. it has not a real function,
  869. * only to check in other gestures if something is detected.
  870. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  871. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  872. *
  873. * @readonly
  874. * @param {Hammer.Instance} inst
  875. * the instance we do the detection for
  876. *
  877. * @readonly
  878. * @param {Object} startEvent
  879. * contains the properties of the first gesture detection in this session.
  880. * Used for calculations about timing, distance, etc.
  881. *
  882. * @readonly
  883. * @param {Object} lastEvent
  884. * contains all the properties of the last gesture detect in this session.
  885. *
  886. * after the gesture detection session has been completed (user has released the screen)
  887. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  888. * this is usefull for gestures like doubletap, where you need to know if the
  889. * previous gesture was a tap
  890. *
  891. * options that have been set by the instance can be received by calling inst.options
  892. *
  893. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  894. * The first param is the name of your gesture, the second the event argument
  895. *
  896. *
  897. * Register gestures
  898. * --------------------
  899. * When an gesture is added to the Hammer.gestures object, it is auto registered
  900. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  901. * manually and pass your gesture object as a param
  902. *
  903. */
  904. /**
  905. * Hold
  906. * Touch stays at the same place for x time
  907. * @events hold
  908. */
  909. Hammer.gestures.Hold = {
  910. name: 'hold',
  911. index: 10,
  912. defaults: {
  913. hold_timeout : 500,
  914. hold_threshold : 1
  915. },
  916. timer: null,
  917. handler: function holdGesture(ev, inst) {
  918. switch(ev.eventType) {
  919. case Hammer.EVENT_START:
  920. // clear any running timers
  921. clearTimeout(this.timer);
  922. // set the gesture so we can check in the timeout if it still is
  923. Hammer.detection.current.name = this.name;
  924. // set timer and if after the timeout it still is hold,
  925. // we trigger the hold event
  926. this.timer = setTimeout(function() {
  927. if(Hammer.detection.current.name == 'hold') {
  928. inst.trigger('hold', ev);
  929. }
  930. }, inst.options.hold_timeout);
  931. break;
  932. // when you move or end we clear the timer
  933. case Hammer.EVENT_MOVE:
  934. if(ev.distance > inst.options.hold_threshold) {
  935. clearTimeout(this.timer);
  936. }
  937. break;
  938. case Hammer.EVENT_END:
  939. clearTimeout(this.timer);
  940. break;
  941. }
  942. }
  943. };
  944. /**
  945. * Tap/DoubleTap
  946. * Quick touch at a place or double at the same place
  947. * @events tap, doubletap
  948. */
  949. Hammer.gestures.Tap = {
  950. name: 'tap',
  951. index: 100,
  952. defaults: {
  953. tap_max_touchtime : 250,
  954. tap_max_distance : 10,
  955. tap_always : true,
  956. doubletap_distance : 20,
  957. doubletap_interval : 300
  958. },
  959. handler: function tapGesture(ev, inst) {
  960. if(ev.eventType == Hammer.EVENT_END) {
  961. // previous gesture, for the double tap since these are two different gesture detections
  962. var prev = Hammer.detection.previous,
  963. did_doubletap = false;
  964. // when the touchtime is higher then the max touch time
  965. // or when the moving distance is too much
  966. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  967. ev.distance > inst.options.tap_max_distance) {
  968. return;
  969. }
  970. // check if double tap
  971. if(prev && prev.name == 'tap' &&
  972. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  973. ev.distance < inst.options.doubletap_distance) {
  974. inst.trigger('doubletap', ev);
  975. did_doubletap = true;
  976. }
  977. // do a single tap
  978. if(!did_doubletap || inst.options.tap_always) {
  979. Hammer.detection.current.name = 'tap';
  980. inst.trigger(Hammer.detection.current.name, ev);
  981. }
  982. }
  983. }
  984. };
  985. /**
  986. * Swipe
  987. * triggers swipe events when the end velocity is above the threshold
  988. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  989. */
  990. Hammer.gestures.Swipe = {
  991. name: 'swipe',
  992. index: 40,
  993. defaults: {
  994. // set 0 for unlimited, but this can conflict with transform
  995. swipe_max_touches : 1,
  996. swipe_velocity : 0.7
  997. },
  998. handler: function swipeGesture(ev, inst) {
  999. if(ev.eventType == Hammer.EVENT_END) {
  1000. // max touches
  1001. if(inst.options.swipe_max_touches > 0 &&
  1002. ev.touches.length > inst.options.swipe_max_touches) {
  1003. return;
  1004. }
  1005. // when the distance we moved is too small we skip this gesture
  1006. // or we can be already in dragging
  1007. if(ev.velocityX > inst.options.swipe_velocity ||
  1008. ev.velocityY > inst.options.swipe_velocity) {
  1009. // trigger swipe events
  1010. inst.trigger(this.name, ev);
  1011. inst.trigger(this.name + ev.direction, ev);
  1012. }
  1013. }
  1014. }
  1015. };
  1016. /**
  1017. * Drag
  1018. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  1019. * moving left and right is a good practice. When all the drag events are blocking
  1020. * you disable scrolling on that area.
  1021. * @events drag, drapleft, dragright, dragup, dragdown
  1022. */
  1023. Hammer.gestures.Drag = {
  1024. name: 'drag',
  1025. index: 50,
  1026. defaults: {
  1027. drag_min_distance : 10,
  1028. // set 0 for unlimited, but this can conflict with transform
  1029. drag_max_touches : 1,
  1030. // prevent default browser behavior when dragging occurs
  1031. // be careful with it, it makes the element a blocking element
  1032. // when you are using the drag gesture, it is a good practice to set this true
  1033. drag_block_horizontal : false,
  1034. drag_block_vertical : false,
  1035. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  1036. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  1037. drag_lock_to_axis : false,
  1038. // drag lock only kicks in when distance > drag_lock_min_distance
  1039. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  1040. drag_lock_min_distance : 25
  1041. },
  1042. triggered: false,
  1043. handler: function dragGesture(ev, inst) {
  1044. // current gesture isnt drag, but dragged is true
  1045. // this means an other gesture is busy. now call dragend
  1046. if(Hammer.detection.current.name != this.name && this.triggered) {
  1047. inst.trigger(this.name +'end', ev);
  1048. this.triggered = false;
  1049. return;
  1050. }
  1051. // max touches
  1052. if(inst.options.drag_max_touches > 0 &&
  1053. ev.touches.length > inst.options.drag_max_touches) {
  1054. return;
  1055. }
  1056. switch(ev.eventType) {
  1057. case Hammer.EVENT_START:
  1058. this.triggered = false;
  1059. break;
  1060. case Hammer.EVENT_MOVE:
  1061. // when the distance we moved is too small we skip this gesture
  1062. // or we can be already in dragging
  1063. if(ev.distance < inst.options.drag_min_distance &&
  1064. Hammer.detection.current.name != this.name) {
  1065. return;
  1066. }
  1067. // we are dragging!
  1068. Hammer.detection.current.name = this.name;
  1069. // lock drag to axis?
  1070. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  1071. ev.drag_locked_to_axis = true;
  1072. }
  1073. var last_direction = Hammer.detection.current.lastEvent.direction;
  1074. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  1075. // keep direction on the axis that the drag gesture started on
  1076. if(Hammer.utils.isVertical(last_direction)) {
  1077. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  1078. }
  1079. else {
  1080. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  1081. }
  1082. }
  1083. // first time, trigger dragstart event
  1084. if(!this.triggered) {
  1085. inst.trigger(this.name +'start', ev);
  1086. this.triggered = true;
  1087. }
  1088. // trigger normal event
  1089. inst.trigger(this.name, ev);
  1090. // direction event, like dragdown
  1091. inst.trigger(this.name + ev.direction, ev);
  1092. // block the browser events
  1093. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  1094. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  1095. ev.preventDefault();
  1096. }
  1097. break;
  1098. case Hammer.EVENT_END:
  1099. // trigger dragend
  1100. if(this.triggered) {
  1101. inst.trigger(this.name +'end', ev);
  1102. }
  1103. this.triggered = false;
  1104. break;
  1105. }
  1106. }
  1107. };
  1108. /**
  1109. * Transform
  1110. * User want to scale or rotate with 2 fingers
  1111. * @events transform, pinch, pinchin, pinchout, rotate
  1112. */
  1113. Hammer.gestures.Transform = {
  1114. name: 'transform',
  1115. index: 45,
  1116. defaults: {
  1117. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  1118. transform_min_scale : 0.01,
  1119. // rotation in degrees
  1120. transform_min_rotation : 1,
  1121. // prevent default browser behavior when two touches are on the screen
  1122. // but it makes the element a blocking element
  1123. // when you are using the transform gesture, it is a good practice to set this true
  1124. transform_always_block : false
  1125. },
  1126. triggered: false,
  1127. handler: function transformGesture(ev, inst) {
  1128. // current gesture isnt drag, but dragged is true
  1129. // this means an other gesture is busy. now call dragend
  1130. if(Hammer.detection.current.name != this.name && this.triggered) {
  1131. inst.trigger(this.name +'end', ev);
  1132. this.triggered = false;
  1133. return;
  1134. }
  1135. // atleast multitouch
  1136. if(ev.touches.length < 2) {
  1137. return;
  1138. }
  1139. // prevent default when two fingers are on the screen
  1140. if(inst.options.transform_always_block) {
  1141. ev.preventDefault();
  1142. }
  1143. switch(ev.eventType) {
  1144. case Hammer.EVENT_START:
  1145. this.triggered = false;
  1146. break;
  1147. case Hammer.EVENT_MOVE:
  1148. var scale_threshold = Math.abs(1-ev.scale);
  1149. var rotation_threshold = Math.abs(ev.rotation);
  1150. // when the distance we moved is too small we skip this gesture
  1151. // or we can be already in dragging
  1152. if(scale_threshold < inst.options.transform_min_scale &&
  1153. rotation_threshold < inst.options.transform_min_rotation) {
  1154. return;
  1155. }
  1156. // we are transforming!
  1157. Hammer.detection.current.name = this.name;
  1158. // first time, trigger dragstart event
  1159. if(!this.triggered) {
  1160. inst.trigger(this.name +'start', ev);
  1161. this.triggered = true;
  1162. }
  1163. inst.trigger(this.name, ev); // basic transform event
  1164. // trigger rotate event
  1165. if(rotation_threshold > inst.options.transform_min_rotation) {
  1166. inst.trigger('rotate', ev);
  1167. }
  1168. // trigger pinch event
  1169. if(scale_threshold > inst.options.transform_min_scale) {
  1170. inst.trigger('pinch', ev);
  1171. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  1172. }
  1173. break;
  1174. case Hammer.EVENT_END:
  1175. // trigger dragend
  1176. if(this.triggered) {
  1177. inst.trigger(this.name +'end', ev);
  1178. }
  1179. this.triggered = false;
  1180. break;
  1181. }
  1182. }
  1183. };
  1184. /**
  1185. * Touch
  1186. * Called as first, tells the user has touched the screen
  1187. * @events touch
  1188. */
  1189. Hammer.gestures.Touch = {
  1190. name: 'touch',
  1191. index: -Infinity,
  1192. defaults: {
  1193. // call preventDefault at touchstart, and makes the element blocking by
  1194. // disabling the scrolling of the page, but it improves gestures like
  1195. // transforming and dragging.
  1196. // be careful with using this, it can be very annoying for users to be stuck
  1197. // on the page
  1198. prevent_default: false,
  1199. // disable mouse events, so only touch (or pen!) input triggers events
  1200. prevent_mouseevents: false
  1201. },
  1202. handler: function touchGesture(ev, inst) {
  1203. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  1204. ev.stopDetect();
  1205. return;
  1206. }
  1207. if(inst.options.prevent_default) {
  1208. ev.preventDefault();
  1209. }
  1210. if(ev.eventType == Hammer.EVENT_START) {
  1211. inst.trigger(this.name, ev);
  1212. }
  1213. }
  1214. };
  1215. /**
  1216. * Release
  1217. * Called as last, tells the user has released the screen
  1218. * @events release
  1219. */
  1220. Hammer.gestures.Release = {
  1221. name: 'release',
  1222. index: Infinity,
  1223. handler: function releaseGesture(ev, inst) {
  1224. if(ev.eventType == Hammer.EVENT_END) {
  1225. inst.trigger(this.name, ev);
  1226. }
  1227. }
  1228. };
  1229. // node export
  1230. if(typeof module === 'object' && typeof module.exports === 'object'){
  1231. module.exports = Hammer;
  1232. }
  1233. // just window export
  1234. else {
  1235. window.Hammer = Hammer;
  1236. // requireJS module definition
  1237. if(typeof window.define === 'function' && window.define.amd) {
  1238. window.define('hammer', [], function() {
  1239. return Hammer;
  1240. });
  1241. }
  1242. }
  1243. })(this);
  1244. },{}],2:[function(require,module,exports){
  1245. //! moment.js
  1246. //! version : 2.4.0
  1247. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  1248. //! license : MIT
  1249. //! momentjs.com
  1250. (function (undefined) {
  1251. /************************************
  1252. Constants
  1253. ************************************/
  1254. var moment,
  1255. VERSION = "2.4.0",
  1256. round = Math.round,
  1257. i,
  1258. YEAR = 0,
  1259. MONTH = 1,
  1260. DATE = 2,
  1261. HOUR = 3,
  1262. MINUTE = 4,
  1263. SECOND = 5,
  1264. MILLISECOND = 6,
  1265. // internal storage for language config files
  1266. languages = {},
  1267. // check for nodeJS
  1268. hasModule = (typeof module !== 'undefined' && module.exports),
  1269. // ASP.NET json date format regex
  1270. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  1271. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  1272. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  1273. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  1274. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  1275. // format tokens
  1276. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  1277. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  1278. // parsing token regexes
  1279. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  1280. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  1281. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  1282. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  1283. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  1284. parseTokenDigits = /\d+/, // nonzero number of digits
  1285. parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
  1286. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  1287. parseTokenT = /T/i, // T (ISO seperator)
  1288. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  1289. // preliminary iso regex
  1290. // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000)
  1291. isoRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,
  1292. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  1293. isoDates = [
  1294. 'YYYY-MM-DD',
  1295. 'GGGG-[W]WW',
  1296. 'GGGG-[W]WW-E',
  1297. 'YYYY-DDD'
  1298. ],
  1299. // iso time formats and regexes
  1300. isoTimes = [
  1301. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  1302. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  1303. ['HH:mm', /(T| )\d\d:\d\d/],
  1304. ['HH', /(T| )\d\d/]
  1305. ],
  1306. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  1307. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  1308. // getter and setter names
  1309. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  1310. unitMillisecondFactors = {
  1311. 'Milliseconds' : 1,
  1312. 'Seconds' : 1e3,
  1313. 'Minutes' : 6e4,
  1314. 'Hours' : 36e5,
  1315. 'Days' : 864e5,
  1316. 'Months' : 2592e6,
  1317. 'Years' : 31536e6
  1318. },
  1319. unitAliases = {
  1320. ms : 'millisecond',
  1321. s : 'second',
  1322. m : 'minute',
  1323. h : 'hour',
  1324. d : 'day',
  1325. D : 'date',
  1326. w : 'week',
  1327. W : 'isoWeek',
  1328. M : 'month',
  1329. y : 'year',
  1330. DDD : 'dayOfYear',
  1331. e : 'weekday',
  1332. E : 'isoWeekday',
  1333. gg: 'weekYear',
  1334. GG: 'isoWeekYear'
  1335. },
  1336. camelFunctions = {
  1337. dayofyear : 'dayOfYear',
  1338. isoweekday : 'isoWeekday',
  1339. isoweek : 'isoWeek',
  1340. weekyear : 'weekYear',
  1341. isoweekyear : 'isoWeekYear'
  1342. },
  1343. // format function strings
  1344. formatFunctions = {},
  1345. // tokens to ordinalize and pad
  1346. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  1347. paddedTokens = 'M D H h m s w W'.split(' '),
  1348. formatTokenFunctions = {
  1349. M : function () {
  1350. return this.month() + 1;
  1351. },
  1352. MMM : function (format) {
  1353. return this.lang().monthsShort(this, format);
  1354. },
  1355. MMMM : function (format) {
  1356. return this.lang().months(this, format);
  1357. },
  1358. D : function () {
  1359. return this.date();
  1360. },
  1361. DDD : function () {
  1362. return this.dayOfYear();
  1363. },
  1364. d : function () {
  1365. return this.day();
  1366. },
  1367. dd : function (format) {
  1368. return this.lang().weekdaysMin(this, format);
  1369. },
  1370. ddd : function (format) {
  1371. return this.lang().weekdaysShort(this, format);
  1372. },
  1373. dddd : function (format) {
  1374. return this.lang().weekdays(this, format);
  1375. },
  1376. w : function () {
  1377. return this.week();
  1378. },
  1379. W : function () {
  1380. return this.isoWeek();
  1381. },
  1382. YY : function () {
  1383. return leftZeroFill(this.year() % 100, 2);
  1384. },
  1385. YYYY : function () {
  1386. return leftZeroFill(this.year(), 4);
  1387. },
  1388. YYYYY : function () {
  1389. return leftZeroFill(this.year(), 5);
  1390. },
  1391. gg : function () {
  1392. return leftZeroFill(this.weekYear() % 100, 2);
  1393. },
  1394. gggg : function () {
  1395. return this.weekYear();
  1396. },
  1397. ggggg : function () {
  1398. return leftZeroFill(this.weekYear(), 5);
  1399. },
  1400. GG : function () {
  1401. return leftZeroFill(this.isoWeekYear() % 100, 2);
  1402. },
  1403. GGGG : function () {
  1404. return this.isoWeekYear();
  1405. },
  1406. GGGGG : function () {
  1407. return leftZeroFill(this.isoWeekYear(), 5);
  1408. },
  1409. e : function () {
  1410. return this.weekday();
  1411. },
  1412. E : function () {
  1413. return this.isoWeekday();
  1414. },
  1415. a : function () {
  1416. return this.lang().meridiem(this.hours(), this.minutes(), true);
  1417. },
  1418. A : function () {
  1419. return this.lang().meridiem(this.hours(), this.minutes(), false);
  1420. },
  1421. H : function () {
  1422. return this.hours();
  1423. },
  1424. h : function () {
  1425. return this.hours() % 12 || 12;
  1426. },
  1427. m : function () {
  1428. return this.minutes();
  1429. },
  1430. s : function () {
  1431. return this.seconds();
  1432. },
  1433. S : function () {
  1434. return toInt(this.milliseconds() / 100);
  1435. },
  1436. SS : function () {
  1437. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  1438. },
  1439. SSS : function () {
  1440. return leftZeroFill(this.milliseconds(), 3);
  1441. },
  1442. SSSS : function () {
  1443. return leftZeroFill(this.milliseconds(), 3);
  1444. },
  1445. Z : function () {
  1446. var a = -this.zone(),
  1447. b = "+";
  1448. if (a < 0) {
  1449. a = -a;
  1450. b = "-";
  1451. }
  1452. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  1453. },
  1454. ZZ : function () {
  1455. var a = -this.zone(),
  1456. b = "+";
  1457. if (a < 0) {
  1458. a = -a;
  1459. b = "-";
  1460. }
  1461. return b + leftZeroFill(toInt(10 * a / 6), 4);
  1462. },
  1463. z : function () {
  1464. return this.zoneAbbr();
  1465. },
  1466. zz : function () {
  1467. return this.zoneName();
  1468. },
  1469. X : function () {
  1470. return this.unix();
  1471. }
  1472. },
  1473. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  1474. function padToken(func, count) {
  1475. return function (a) {
  1476. return leftZeroFill(func.call(this, a), count);
  1477. };
  1478. }
  1479. function ordinalizeToken(func, period) {
  1480. return function (a) {
  1481. return this.lang().ordinal(func.call(this, a), period);
  1482. };
  1483. }
  1484. while (ordinalizeTokens.length) {
  1485. i = ordinalizeTokens.pop();
  1486. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  1487. }
  1488. while (paddedTokens.length) {
  1489. i = paddedTokens.pop();
  1490. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  1491. }
  1492. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  1493. /************************************
  1494. Constructors
  1495. ************************************/
  1496. function Language() {
  1497. }
  1498. // Moment prototype object
  1499. function Moment(config) {
  1500. checkOverflow(config);
  1501. extend(this, config);
  1502. }
  1503. // Duration Constructor
  1504. function Duration(duration) {
  1505. var normalizedInput = normalizeObjectUnits(duration),
  1506. years = normalizedInput.year || 0,
  1507. months = normalizedInput.month || 0,
  1508. weeks = normalizedInput.week || 0,
  1509. days = normalizedInput.day || 0,
  1510. hours = normalizedInput.hour || 0,
  1511. minutes = normalizedInput.minute || 0,
  1512. seconds = normalizedInput.second || 0,
  1513. milliseconds = normalizedInput.millisecond || 0;
  1514. // store reference to input for deterministic cloning
  1515. this._input = duration;
  1516. // representation for dateAddRemove
  1517. this._milliseconds = +milliseconds +
  1518. seconds * 1e3 + // 1000
  1519. minutes * 6e4 + // 1000 * 60
  1520. hours * 36e5; // 1000 * 60 * 60
  1521. // Because of dateAddRemove treats 24 hours as different from a
  1522. // day when working around DST, we need to store them separately
  1523. this._days = +days +
  1524. weeks * 7;
  1525. // It is impossible translate months into days without knowing
  1526. // which months you are are talking about, so we have to store
  1527. // it separately.
  1528. this._months = +months +
  1529. years * 12;
  1530. this._data = {};
  1531. this._bubble();
  1532. }
  1533. /************************************
  1534. Helpers
  1535. ************************************/
  1536. function extend(a, b) {
  1537. for (var i in b) {
  1538. if (b.hasOwnProperty(i)) {
  1539. a[i] = b[i];
  1540. }
  1541. }
  1542. if (b.hasOwnProperty("toString")) {
  1543. a.toString = b.toString;
  1544. }
  1545. if (b.hasOwnProperty("valueOf")) {
  1546. a.valueOf = b.valueOf;
  1547. }
  1548. return a;
  1549. }
  1550. function absRound(number) {
  1551. if (number < 0) {
  1552. return Math.ceil(number);
  1553. } else {
  1554. return Math.floor(number);
  1555. }
  1556. }
  1557. // left zero fill a number
  1558. // see http://jsperf.com/left-zero-filling for performance comparison
  1559. function leftZeroFill(number, targetLength) {
  1560. var output = number + '';
  1561. while (output.length < targetLength) {
  1562. output = '0' + output;
  1563. }
  1564. return output;
  1565. }
  1566. // helper function for _.addTime and _.subtractTime
  1567. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  1568. var milliseconds = duration._milliseconds,
  1569. days = duration._days,
  1570. months = duration._months,
  1571. minutes,
  1572. hours;
  1573. if (milliseconds) {
  1574. mom._d.setTime(+mom._d + milliseconds * isAdding);
  1575. }
  1576. // store the minutes and hours so we can restore them
  1577. if (days || months) {
  1578. minutes = mom.minute();
  1579. hours = mom.hour();
  1580. }
  1581. if (days) {
  1582. mom.date(mom.date() + days * isAdding);
  1583. }
  1584. if (months) {
  1585. mom.month(mom.month() + months * isAdding);
  1586. }
  1587. if (milliseconds && !ignoreUpdateOffset) {
  1588. moment.updateOffset(mom);
  1589. }
  1590. // restore the minutes and hours after possibly changing dst
  1591. if (days || months) {
  1592. mom.minute(minutes);
  1593. mom.hour(hours);
  1594. }
  1595. }
  1596. // check if is an array
  1597. function isArray(input) {
  1598. return Object.prototype.toString.call(input) === '[object Array]';
  1599. }
  1600. function isDate(input) {
  1601. return Object.prototype.toString.call(input) === '[object Date]' ||
  1602. input instanceof Date;
  1603. }
  1604. // compare two arrays, return the number of differences
  1605. function compareArrays(array1, array2, dontConvert) {
  1606. var len = Math.min(array1.length, array2.length),
  1607. lengthDiff = Math.abs(array1.length - array2.length),
  1608. diffs = 0,
  1609. i;
  1610. for (i = 0; i < len; i++) {
  1611. if ((dontConvert && array1[i] !== array2[i]) ||
  1612. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  1613. diffs++;
  1614. }
  1615. }
  1616. return diffs + lengthDiff;
  1617. }
  1618. function normalizeUnits(units) {
  1619. if (units) {
  1620. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  1621. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  1622. }
  1623. return units;
  1624. }
  1625. function normalizeObjectUnits(inputObject) {
  1626. var normalizedInput = {},
  1627. normalizedProp,
  1628. prop,
  1629. index;
  1630. for (prop in inputObject) {
  1631. if (inputObject.hasOwnProperty(prop)) {
  1632. normalizedProp = normalizeUnits(prop);
  1633. if (normalizedProp) {
  1634. normalizedInput[normalizedProp] = inputObject[prop];
  1635. }
  1636. }
  1637. }
  1638. return normalizedInput;
  1639. }
  1640. function makeList(field) {
  1641. var count, setter;
  1642. if (field.indexOf('week') === 0) {
  1643. count = 7;
  1644. setter = 'day';
  1645. }
  1646. else if (field.indexOf('month') === 0) {
  1647. count = 12;
  1648. setter = 'month';
  1649. }
  1650. else {
  1651. return;
  1652. }
  1653. moment[field] = function (format, index) {
  1654. var i, getter,
  1655. method = moment.fn._lang[field],
  1656. results = [];
  1657. if (typeof format === 'number') {
  1658. index = format;
  1659. format = undefined;
  1660. }
  1661. getter = function (i) {
  1662. var m = moment().utc().set(setter, i);
  1663. return method.call(moment.fn._lang, m, format || '');
  1664. };
  1665. if (index != null) {
  1666. return getter(index);
  1667. }
  1668. else {
  1669. for (i = 0; i < count; i++) {
  1670. results.push(getter(i));
  1671. }
  1672. return results;
  1673. }
  1674. };
  1675. }
  1676. function toInt(argumentForCoercion) {
  1677. var coercedNumber = +argumentForCoercion,
  1678. value = 0;
  1679. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  1680. if (coercedNumber >= 0) {
  1681. value = Math.floor(coercedNumber);
  1682. } else {
  1683. value = Math.ceil(coercedNumber);
  1684. }
  1685. }
  1686. return value;
  1687. }
  1688. function daysInMonth(year, month) {
  1689. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  1690. }
  1691. function daysInYear(year) {
  1692. return isLeapYear(year) ? 366 : 365;
  1693. }
  1694. function isLeapYear(year) {
  1695. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  1696. }
  1697. function checkOverflow(m) {
  1698. var overflow;
  1699. if (m._a && m._pf.overflow === -2) {
  1700. overflow =
  1701. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  1702. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  1703. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  1704. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  1705. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  1706. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  1707. -1;
  1708. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  1709. overflow = DATE;
  1710. }
  1711. m._pf.overflow = overflow;
  1712. }
  1713. }
  1714. function initializeParsingFlags(config) {
  1715. config._pf = {
  1716. empty : false,
  1717. unusedTokens : [],
  1718. unusedInput : [],
  1719. overflow : -2,
  1720. charsLeftOver : 0,
  1721. nullInput : false,
  1722. invalidMonth : null,
  1723. invalidFormat : false,
  1724. userInvalidated : false,
  1725. iso: false
  1726. };
  1727. }
  1728. function isValid(m) {
  1729. if (m._isValid == null) {
  1730. m._isValid = !isNaN(m._d.getTime()) &&
  1731. m._pf.overflow < 0 &&
  1732. !m._pf.empty &&
  1733. !m._pf.invalidMonth &&
  1734. !m._pf.nullInput &&
  1735. !m._pf.invalidFormat &&
  1736. !m._pf.userInvalidated;
  1737. if (m._strict) {
  1738. m._isValid = m._isValid &&
  1739. m._pf.charsLeftOver === 0 &&
  1740. m._pf.unusedTokens.length === 0;
  1741. }
  1742. }
  1743. return m._isValid;
  1744. }
  1745. function normalizeLanguage(key) {
  1746. return key ? key.toLowerCase().replace('_', '-') : key;
  1747. }
  1748. /************************************
  1749. Languages
  1750. ************************************/
  1751. extend(Language.prototype, {
  1752. set : function (config) {
  1753. var prop, i;
  1754. for (i in config) {
  1755. prop = config[i];
  1756. if (typeof prop === 'function') {
  1757. this[i] = prop;
  1758. } else {
  1759. this['_' + i] = prop;
  1760. }
  1761. }
  1762. },
  1763. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  1764. months : function (m) {
  1765. return this._months[m.month()];
  1766. },
  1767. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  1768. monthsShort : function (m) {
  1769. return this._monthsShort[m.month()];
  1770. },
  1771. monthsParse : function (monthName) {
  1772. var i, mom, regex;
  1773. if (!this._monthsParse) {
  1774. this._monthsParse = [];
  1775. }
  1776. for (i = 0; i < 12; i++) {
  1777. // make the regex if we don't have it already
  1778. if (!this._monthsParse[i]) {
  1779. mom = moment.utc([2000, i]);
  1780. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  1781. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1782. }
  1783. // test the regex
  1784. if (this._monthsParse[i].test(monthName)) {
  1785. return i;
  1786. }
  1787. }
  1788. },
  1789. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  1790. weekdays : function (m) {
  1791. return this._weekdays[m.day()];
  1792. },
  1793. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  1794. weekdaysShort : function (m) {
  1795. return this._weekdaysShort[m.day()];
  1796. },
  1797. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  1798. weekdaysMin : function (m) {
  1799. return this._weekdaysMin[m.day()];
  1800. },
  1801. weekdaysParse : function (weekdayName) {
  1802. var i, mom, regex;
  1803. if (!this._weekdaysParse) {
  1804. this._weekdaysParse = [];
  1805. }
  1806. for (i = 0; i < 7; i++) {
  1807. // make the regex if we don't have it already
  1808. if (!this._weekdaysParse[i]) {
  1809. mom = moment([2000, 1]).day(i);
  1810. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  1811. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1812. }
  1813. // test the regex
  1814. if (this._weekdaysParse[i].test(weekdayName)) {
  1815. return i;
  1816. }
  1817. }
  1818. },
  1819. _longDateFormat : {
  1820. LT : "h:mm A",
  1821. L : "MM/DD/YYYY",
  1822. LL : "MMMM D YYYY",
  1823. LLL : "MMMM D YYYY LT",
  1824. LLLL : "dddd, MMMM D YYYY LT"
  1825. },
  1826. longDateFormat : function (key) {
  1827. var output = this._longDateFormat[key];
  1828. if (!output && this._longDateFormat[key.toUpperCase()]) {
  1829. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  1830. return val.slice(1);
  1831. });
  1832. this._longDateFormat[key] = output;
  1833. }
  1834. return output;
  1835. },
  1836. isPM : function (input) {
  1837. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  1838. // Using charAt should be more compatible.
  1839. return ((input + '').toLowerCase().charAt(0) === 'p');
  1840. },
  1841. _meridiemParse : /[ap]\.?m?\.?/i,
  1842. meridiem : function (hours, minutes, isLower) {
  1843. if (hours > 11) {
  1844. return isLower ? 'pm' : 'PM';
  1845. } else {
  1846. return isLower ? 'am' : 'AM';
  1847. }
  1848. },
  1849. _calendar : {
  1850. sameDay : '[Today at] LT',
  1851. nextDay : '[Tomorrow at] LT',
  1852. nextWeek : 'dddd [at] LT',
  1853. lastDay : '[Yesterday at] LT',
  1854. lastWeek : '[Last] dddd [at] LT',
  1855. sameElse : 'L'
  1856. },
  1857. calendar : function (key, mom) {
  1858. var output = this._calendar[key];
  1859. return typeof output === 'function' ? output.apply(mom) : output;
  1860. },
  1861. _relativeTime : {
  1862. future : "in %s",
  1863. past : "%s ago",
  1864. s : "a few seconds",
  1865. m : "a minute",
  1866. mm : "%d minutes",
  1867. h : "an hour",
  1868. hh : "%d hours",
  1869. d : "a day",
  1870. dd : "%d days",
  1871. M : "a month",
  1872. MM : "%d months",
  1873. y : "a year",
  1874. yy : "%d years"
  1875. },
  1876. relativeTime : function (number, withoutSuffix, string, isFuture) {
  1877. var output = this._relativeTime[string];
  1878. return (typeof output === 'function') ?
  1879. output(number, withoutSuffix, string, isFuture) :
  1880. output.replace(/%d/i, number);
  1881. },
  1882. pastFuture : function (diff, output) {
  1883. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  1884. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  1885. },
  1886. ordinal : function (number) {
  1887. return this._ordinal.replace("%d", number);
  1888. },
  1889. _ordinal : "%d",
  1890. preparse : function (string) {
  1891. return string;
  1892. },
  1893. postformat : function (string) {
  1894. return string;
  1895. },
  1896. week : function (mom) {
  1897. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  1898. },
  1899. _week : {
  1900. dow : 0, // Sunday is the first day of the week.
  1901. doy : 6 // The week that contains Jan 1st is the first week of the year.
  1902. },
  1903. _invalidDate: 'Invalid date',
  1904. invalidDate: function () {
  1905. return this._invalidDate;
  1906. }
  1907. });
  1908. // Loads a language definition into the `languages` cache. The function
  1909. // takes a key and optionally values. If not in the browser and no values
  1910. // are provided, it will load the language file module. As a convenience,
  1911. // this function also returns the language values.
  1912. function loadLang(key, values) {
  1913. values.abbr = key;
  1914. if (!languages[key]) {
  1915. languages[key] = new Language();
  1916. }
  1917. languages[key].set(values);
  1918. return languages[key];
  1919. }
  1920. // Remove a language from the `languages` cache. Mostly useful in tests.
  1921. function unloadLang(key) {
  1922. delete languages[key];
  1923. }
  1924. // Determines which language definition to use and returns it.
  1925. //
  1926. // With no parameters, it will return the global language. If you
  1927. // pass in a language key, such as 'en', it will return the
  1928. // definition for 'en', so long as 'en' has already been loaded using
  1929. // moment.lang.
  1930. function getLangDefinition(key) {
  1931. var i = 0, j, lang, next, split,
  1932. get = function (k) {
  1933. if (!languages[k] && hasModule) {
  1934. try {
  1935. require('./lang/' + k);
  1936. } catch (e) { }
  1937. }
  1938. return languages[k];
  1939. };
  1940. if (!key) {
  1941. return moment.fn._lang;
  1942. }
  1943. if (!isArray(key)) {
  1944. //short-circuit everything else
  1945. lang = get(key);
  1946. if (lang) {
  1947. return lang;
  1948. }
  1949. key = [key];
  1950. }
  1951. //pick the language from the array
  1952. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  1953. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  1954. while (i < key.length) {
  1955. split = normalizeLanguage(key[i]).split('-');
  1956. j = split.length;
  1957. next = normalizeLanguage(key[i + 1]);
  1958. next = next ? next.split('-') : null;
  1959. while (j > 0) {
  1960. lang = get(split.slice(0, j).join('-'));
  1961. if (lang) {
  1962. return lang;
  1963. }
  1964. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  1965. //the next array item is better than a shallower substring of this one
  1966. break;
  1967. }
  1968. j--;
  1969. }
  1970. i++;
  1971. }
  1972. return moment.fn._lang;
  1973. }
  1974. /************************************
  1975. Formatting
  1976. ************************************/
  1977. function removeFormattingTokens(input) {
  1978. if (input.match(/\[[\s\S]/)) {
  1979. return input.replace(/^\[|\]$/g, "");
  1980. }
  1981. return input.replace(/\\/g, "");
  1982. }
  1983. function makeFormatFunction(format) {
  1984. var array = format.match(formattingTokens), i, length;
  1985. for (i = 0, length = array.length; i < length; i++) {
  1986. if (formatTokenFunctions[array[i]]) {
  1987. array[i] = formatTokenFunctions[array[i]];
  1988. } else {
  1989. array[i] = removeFormattingTokens(array[i]);
  1990. }
  1991. }
  1992. return function (mom) {
  1993. var output = "";
  1994. for (i = 0; i < length; i++) {
  1995. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  1996. }
  1997. return output;
  1998. };
  1999. }
  2000. // format date using native date object
  2001. function formatMoment(m, format) {
  2002. if (!m.isValid()) {
  2003. return m.lang().invalidDate();
  2004. }
  2005. format = expandFormat(format, m.lang());
  2006. if (!formatFunctions[format]) {
  2007. formatFunctions[format] = makeFormatFunction(format);
  2008. }
  2009. return formatFunctions[format](m);
  2010. }
  2011. function expandFormat(format, lang) {
  2012. var i = 5;
  2013. function replaceLongDateFormatTokens(input) {
  2014. return lang.longDateFormat(input) || input;
  2015. }
  2016. localFormattingTokens.lastIndex = 0;
  2017. while (i >= 0 && localFormattingTokens.test(format)) {
  2018. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  2019. localFormattingTokens.lastIndex = 0;
  2020. i -= 1;
  2021. }
  2022. return format;
  2023. }
  2024. /************************************
  2025. Parsing
  2026. ************************************/
  2027. // get the regex to find the next token
  2028. function getParseRegexForToken(token, config) {
  2029. var a;
  2030. switch (token) {
  2031. case 'DDDD':
  2032. return parseTokenThreeDigits;
  2033. case 'YYYY':
  2034. case 'GGGG':
  2035. case 'gggg':
  2036. return parseTokenFourDigits;
  2037. case 'YYYYY':
  2038. case 'GGGGG':
  2039. case 'ggggg':
  2040. return parseTokenSixDigits;
  2041. case 'S':
  2042. case 'SS':
  2043. case 'SSS':
  2044. case 'DDD':
  2045. return parseTokenOneToThreeDigits;
  2046. case 'MMM':
  2047. case 'MMMM':
  2048. case 'dd':
  2049. case 'ddd':
  2050. case 'dddd':
  2051. return parseTokenWord;
  2052. case 'a':
  2053. case 'A':
  2054. return getLangDefinition(config._l)._meridiemParse;
  2055. case 'X':
  2056. return parseTokenTimestampMs;
  2057. case 'Z':
  2058. case 'ZZ':
  2059. return parseTokenTimezone;
  2060. case 'T':
  2061. return parseTokenT;
  2062. case 'SSSS':
  2063. return parseTokenDigits;
  2064. case 'MM':
  2065. case 'DD':
  2066. case 'YY':
  2067. case 'GG':
  2068. case 'gg':
  2069. case 'HH':
  2070. case 'hh':
  2071. case 'mm':
  2072. case 'ss':
  2073. case 'M':
  2074. case 'D':
  2075. case 'd':
  2076. case 'H':
  2077. case 'h':
  2078. case 'm':
  2079. case 's':
  2080. case 'w':
  2081. case 'ww':
  2082. case 'W':
  2083. case 'WW':
  2084. case 'e':
  2085. case 'E':
  2086. return parseTokenOneOrTwoDigits;
  2087. default :
  2088. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  2089. return a;
  2090. }
  2091. }
  2092. function timezoneMinutesFromString(string) {
  2093. var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
  2094. parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  2095. minutes = +(parts[1] * 60) + toInt(parts[2]);
  2096. return parts[0] === '+' ? -minutes : minutes;
  2097. }
  2098. // function to convert string input to date
  2099. function addTimeToArrayFromToken(token, input, config) {
  2100. var a, datePartArray = config._a;
  2101. switch (token) {
  2102. // MONTH
  2103. case 'M' : // fall through to MM
  2104. case 'MM' :
  2105. if (input != null) {
  2106. datePartArray[MONTH] = toInt(input) - 1;
  2107. }
  2108. break;
  2109. case 'MMM' : // fall through to MMMM
  2110. case 'MMMM' :
  2111. a = getLangDefinition(config._l).monthsParse(input);
  2112. // if we didn't find a month name, mark the date as invalid.
  2113. if (a != null) {
  2114. datePartArray[MONTH] = a;
  2115. } else {
  2116. config._pf.invalidMonth = input;
  2117. }
  2118. break;
  2119. // DAY OF MONTH
  2120. case 'D' : // fall through to DD
  2121. case 'DD' :
  2122. if (input != null) {
  2123. datePartArray[DATE] = toInt(input);
  2124. }
  2125. break;
  2126. // DAY OF YEAR
  2127. case 'DDD' : // fall through to DDDD
  2128. case 'DDDD' :
  2129. if (input != null) {
  2130. config._dayOfYear = toInt(input);
  2131. }
  2132. break;
  2133. // YEAR
  2134. case 'YY' :
  2135. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  2136. break;
  2137. case 'YYYY' :
  2138. case 'YYYYY' :
  2139. datePartArray[YEAR] = toInt(input);
  2140. break;
  2141. // AM / PM
  2142. case 'a' : // fall through to A
  2143. case 'A' :
  2144. config._isPm = getLangDefinition(config._l).isPM(input);
  2145. break;
  2146. // 24 HOUR
  2147. case 'H' : // fall through to hh
  2148. case 'HH' : // fall through to hh
  2149. case 'h' : // fall through to hh
  2150. case 'hh' :
  2151. datePartArray[HOUR] = toInt(input);
  2152. break;
  2153. // MINUTE
  2154. case 'm' : // fall through to mm
  2155. case 'mm' :
  2156. datePartArray[MINUTE] = toInt(input);
  2157. break;
  2158. // SECOND
  2159. case 's' : // fall through to ss
  2160. case 'ss' :
  2161. datePartArray[SECOND] = toInt(input);
  2162. break;
  2163. // MILLISECOND
  2164. case 'S' :
  2165. case 'SS' :
  2166. case 'SSS' :
  2167. case 'SSSS' :
  2168. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  2169. break;
  2170. // UNIX TIMESTAMP WITH MS
  2171. case 'X':
  2172. config._d = new Date(parseFloat(input) * 1000);
  2173. break;
  2174. // TIMEZONE
  2175. case 'Z' : // fall through to ZZ
  2176. case 'ZZ' :
  2177. config._useUTC = true;
  2178. config._tzm = timezoneMinutesFromString(input);
  2179. break;
  2180. case 'w':
  2181. case 'ww':
  2182. case 'W':
  2183. case 'WW':
  2184. case 'd':
  2185. case 'dd':
  2186. case 'ddd':
  2187. case 'dddd':
  2188. case 'e':
  2189. case 'E':
  2190. token = token.substr(0, 1);
  2191. /* falls through */
  2192. case 'gg':
  2193. case 'gggg':
  2194. case 'GG':
  2195. case 'GGGG':
  2196. case 'GGGGG':
  2197. token = token.substr(0, 2);
  2198. if (input) {
  2199. config._w = config._w || {};
  2200. config._w[token] = input;
  2201. }
  2202. break;
  2203. }
  2204. }
  2205. // convert an array to a date.
  2206. // the array should mirror the parameters below
  2207. // note: all values past the year are optional and will default to the lowest possible value.
  2208. // [year, month, day , hour, minute, second, millisecond]
  2209. function dateFromConfig(config) {
  2210. var i, date, input = [], currentDate,
  2211. yearToUse, fixYear, w, temp, lang, weekday, week;
  2212. if (config._d) {
  2213. return;
  2214. }
  2215. currentDate = currentDateArray(config);
  2216. //compute day of the year from weeks and weekdays
  2217. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  2218. fixYear = function (val) {
  2219. return val ?
  2220. (val.length < 3 ? (parseInt(val, 10) > 68 ? '19' + val : '20' + val) : val) :
  2221. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  2222. };
  2223. w = config._w;
  2224. if (w.GG != null || w.W != null || w.E != null) {
  2225. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  2226. }
  2227. else {
  2228. lang = getLangDefinition(config._l);
  2229. weekday = w.d != null ? parseWeekday(w.d, lang) :
  2230. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  2231. week = parseInt(w.w, 10) || 1;
  2232. //if we're parsing 'd', then the low day numbers may be next week
  2233. if (w.d != null && weekday < lang._week.dow) {
  2234. week++;
  2235. }
  2236. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  2237. }
  2238. config._a[YEAR] = temp.year;
  2239. config._dayOfYear = temp.dayOfYear;
  2240. }
  2241. //if the day of the year is set, figure out what it is
  2242. if (config._dayOfYear) {
  2243. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  2244. if (config._dayOfYear > daysInYear(yearToUse)) {
  2245. config._pf._overflowDayOfYear = true;
  2246. }
  2247. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  2248. config._a[MONTH] = date.getUTCMonth();
  2249. config._a[DATE] = date.getUTCDate();
  2250. }
  2251. // Default to current date.
  2252. // * if no year, month, day of month are given, default to today
  2253. // * if day of month is given, default month and year
  2254. // * if month is given, default only year
  2255. // * if year is given, don't default anything
  2256. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  2257. config._a[i] = input[i] = currentDate[i];
  2258. }
  2259. // Zero out whatever was not defaulted, including time
  2260. for (; i < 7; i++) {
  2261. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  2262. }
  2263. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  2264. input[HOUR] += toInt((config._tzm || 0) / 60);
  2265. input[MINUTE] += toInt((config._tzm || 0) % 60);
  2266. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  2267. }
  2268. function dateFromObject(config) {
  2269. var normalizedInput;
  2270. if (config._d) {
  2271. return;
  2272. }
  2273. normalizedInput = normalizeObjectUnits(config._i);
  2274. config._a = [
  2275. normalizedInput.year,
  2276. normalizedInput.month,
  2277. normalizedInput.day,
  2278. normalizedInput.hour,
  2279. normalizedInput.minute,
  2280. normalizedInput.second,
  2281. normalizedInput.millisecond
  2282. ];
  2283. dateFromConfig(config);
  2284. }
  2285. function currentDateArray(config) {
  2286. var now = new Date();
  2287. if (config._useUTC) {
  2288. return [
  2289. now.getUTCFullYear(),
  2290. now.getUTCMonth(),
  2291. now.getUTCDate()
  2292. ];
  2293. } else {
  2294. return [now.getFullYear(), now.getMonth(), now.getDate()];
  2295. }
  2296. }
  2297. // date from string and format string
  2298. function makeDateFromStringAndFormat(config) {
  2299. config._a = [];
  2300. config._pf.empty = true;
  2301. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  2302. var lang = getLangDefinition(config._l),
  2303. string = '' + config._i,
  2304. i, parsedInput, tokens, token, skipped,
  2305. stringLength = string.length,
  2306. totalParsedInputLength = 0;
  2307. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  2308. for (i = 0; i < tokens.length; i++) {
  2309. token = tokens[i];
  2310. parsedInput = (getParseRegexForToken(token, config).exec(string) || [])[0];
  2311. if (parsedInput) {
  2312. skipped = string.substr(0, string.indexOf(parsedInput));
  2313. if (skipped.length > 0) {
  2314. config._pf.unusedInput.push(skipped);
  2315. }
  2316. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  2317. totalParsedInputLength += parsedInput.length;
  2318. }
  2319. // don't parse if it's not a known token
  2320. if (formatTokenFunctions[token]) {
  2321. if (parsedInput) {
  2322. config._pf.empty = false;
  2323. }
  2324. else {
  2325. config._pf.unusedTokens.push(token);
  2326. }
  2327. addTimeToArrayFromToken(token, parsedInput, config);
  2328. }
  2329. else if (config._strict && !parsedInput) {
  2330. config._pf.unusedTokens.push(token);
  2331. }
  2332. }
  2333. // add remaining unparsed input length to the string
  2334. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  2335. if (string.length > 0) {
  2336. config._pf.unusedInput.push(string);
  2337. }
  2338. // handle am pm
  2339. if (config._isPm && config._a[HOUR] < 12) {
  2340. config._a[HOUR] += 12;
  2341. }
  2342. // if is 12 am, change hours to 0
  2343. if (config._isPm === false && config._a[HOUR] === 12) {
  2344. config._a[HOUR] = 0;
  2345. }
  2346. dateFromConfig(config);
  2347. checkOverflow(config);
  2348. }
  2349. function unescapeFormat(s) {
  2350. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  2351. return p1 || p2 || p3 || p4;
  2352. });
  2353. }
  2354. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  2355. function regexpEscape(s) {
  2356. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  2357. }
  2358. // date from string and array of format strings
  2359. function makeDateFromStringAndArray(config) {
  2360. var tempConfig,
  2361. bestMoment,
  2362. scoreToBeat,
  2363. i,
  2364. currentScore;
  2365. if (config._f.length === 0) {
  2366. config._pf.invalidFormat = true;
  2367. config._d = new Date(NaN);
  2368. return;
  2369. }
  2370. for (i = 0; i < config._f.length; i++) {
  2371. currentScore = 0;
  2372. tempConfig = extend({}, config);
  2373. initializeParsingFlags(tempConfig);
  2374. tempConfig._f = config._f[i];
  2375. makeDateFromStringAndFormat(tempConfig);
  2376. if (!isValid(tempConfig)) {
  2377. continue;
  2378. }
  2379. // if there is any input that was not parsed add a penalty for that format
  2380. currentScore += tempConfig._pf.charsLeftOver;
  2381. //or tokens
  2382. currentScore += tempConfig._pf.unusedTokens.length * 10;
  2383. tempConfig._pf.score = currentScore;
  2384. if (scoreToBeat == null || currentScore < scoreToBeat) {
  2385. scoreToBeat = currentScore;
  2386. bestMoment = tempConfig;
  2387. }
  2388. }
  2389. extend(config, bestMoment || tempConfig);
  2390. }
  2391. // date from iso format
  2392. function makeDateFromString(config) {
  2393. var i,
  2394. string = config._i,
  2395. match = isoRegex.exec(string);
  2396. if (match) {
  2397. config._pf.iso = true;
  2398. for (i = 4; i > 0; i--) {
  2399. if (match[i]) {
  2400. // match[5] should be "T" or undefined
  2401. config._f = isoDates[i - 1] + (match[6] || " ");
  2402. break;
  2403. }
  2404. }
  2405. for (i = 0; i < 4; i++) {
  2406. if (isoTimes[i][1].exec(string)) {
  2407. config._f += isoTimes[i][0];
  2408. break;
  2409. }
  2410. }
  2411. if (parseTokenTimezone.exec(string)) {
  2412. config._f += "Z";
  2413. }
  2414. makeDateFromStringAndFormat(config);
  2415. }
  2416. else {
  2417. config._d = new Date(string);
  2418. }
  2419. }
  2420. function makeDateFromInput(config) {
  2421. var input = config._i,
  2422. matched = aspNetJsonRegex.exec(input);
  2423. if (input === undefined) {
  2424. config._d = new Date();
  2425. } else if (matched) {
  2426. config._d = new Date(+matched[1]);
  2427. } else if (typeof input === 'string') {
  2428. makeDateFromString(config);
  2429. } else if (isArray(input)) {
  2430. config._a = input.slice(0);
  2431. dateFromConfig(config);
  2432. } else if (isDate(input)) {
  2433. config._d = new Date(+input);
  2434. } else if (typeof(input) === 'object') {
  2435. dateFromObject(config);
  2436. } else {
  2437. config._d = new Date(input);
  2438. }
  2439. }
  2440. function makeDate(y, m, d, h, M, s, ms) {
  2441. //can't just apply() to create a date:
  2442. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  2443. var date = new Date(y, m, d, h, M, s, ms);
  2444. //the date constructor doesn't accept years < 1970
  2445. if (y < 1970) {
  2446. date.setFullYear(y);
  2447. }
  2448. return date;
  2449. }
  2450. function makeUTCDate(y) {
  2451. var date = new Date(Date.UTC.apply(null, arguments));
  2452. if (y < 1970) {
  2453. date.setUTCFullYear(y);
  2454. }
  2455. return date;
  2456. }
  2457. function parseWeekday(input, language) {
  2458. if (typeof input === 'string') {
  2459. if (!isNaN(input)) {
  2460. input = parseInt(input, 10);
  2461. }
  2462. else {
  2463. input = language.weekdaysParse(input);
  2464. if (typeof input !== 'number') {
  2465. return null;
  2466. }
  2467. }
  2468. }
  2469. return input;
  2470. }
  2471. /************************************
  2472. Relative Time
  2473. ************************************/
  2474. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  2475. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  2476. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  2477. }
  2478. function relativeTime(milliseconds, withoutSuffix, lang) {
  2479. var seconds = round(Math.abs(milliseconds) / 1000),
  2480. minutes = round(seconds / 60),
  2481. hours = round(minutes / 60),
  2482. days = round(hours / 24),
  2483. years = round(days / 365),
  2484. args = seconds < 45 && ['s', seconds] ||
  2485. minutes === 1 && ['m'] ||
  2486. minutes < 45 && ['mm', minutes] ||
  2487. hours === 1 && ['h'] ||
  2488. hours < 22 && ['hh', hours] ||
  2489. days === 1 && ['d'] ||
  2490. days <= 25 && ['dd', days] ||
  2491. days <= 45 && ['M'] ||
  2492. days < 345 && ['MM', round(days / 30)] ||
  2493. years === 1 && ['y'] || ['yy', years];
  2494. args[2] = withoutSuffix;
  2495. args[3] = milliseconds > 0;
  2496. args[4] = lang;
  2497. return substituteTimeAgo.apply({}, args);
  2498. }
  2499. /************************************
  2500. Week of Year
  2501. ************************************/
  2502. // firstDayOfWeek 0 = sun, 6 = sat
  2503. // the day of the week that starts the week
  2504. // (usually sunday or monday)
  2505. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  2506. // the first week is the week that contains the first
  2507. // of this day of the week
  2508. // (eg. ISO weeks use thursday (4))
  2509. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  2510. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  2511. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  2512. adjustedMoment;
  2513. if (daysToDayOfWeek > end) {
  2514. daysToDayOfWeek -= 7;
  2515. }
  2516. if (daysToDayOfWeek < end - 7) {
  2517. daysToDayOfWeek += 7;
  2518. }
  2519. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  2520. return {
  2521. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  2522. year: adjustedMoment.year()
  2523. };
  2524. }
  2525. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  2526. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  2527. var d = new Date(Date.UTC(year, 0)).getUTCDay(),
  2528. daysToAdd, dayOfYear;
  2529. weekday = weekday != null ? weekday : firstDayOfWeek;
  2530. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0);
  2531. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  2532. return {
  2533. year: dayOfYear > 0 ? year : year - 1,
  2534. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  2535. };
  2536. }
  2537. /************************************
  2538. Top Level Functions
  2539. ************************************/
  2540. function makeMoment(config) {
  2541. var input = config._i,
  2542. format = config._f;
  2543. if (typeof config._pf === 'undefined') {
  2544. initializeParsingFlags(config);
  2545. }
  2546. if (input === null) {
  2547. return moment.invalid({nullInput: true});
  2548. }
  2549. if (typeof input === 'string') {
  2550. config._i = input = getLangDefinition().preparse(input);
  2551. }
  2552. if (moment.isMoment(input)) {
  2553. config = extend({}, input);
  2554. config._d = new Date(+input._d);
  2555. } else if (format) {
  2556. if (isArray(format)) {
  2557. makeDateFromStringAndArray(config);
  2558. } else {
  2559. makeDateFromStringAndFormat(config);
  2560. }
  2561. } else {
  2562. makeDateFromInput(config);
  2563. }
  2564. return new Moment(config);
  2565. }
  2566. moment = function (input, format, lang, strict) {
  2567. if (typeof(lang) === "boolean") {
  2568. strict = lang;
  2569. lang = undefined;
  2570. }
  2571. return makeMoment({
  2572. _i : input,
  2573. _f : format,
  2574. _l : lang,
  2575. _strict : strict,
  2576. _isUTC : false
  2577. });
  2578. };
  2579. // creating with utc
  2580. moment.utc = function (input, format, lang, strict) {
  2581. var m;
  2582. if (typeof(lang) === "boolean") {
  2583. strict = lang;
  2584. lang = undefined;
  2585. }
  2586. m = makeMoment({
  2587. _useUTC : true,
  2588. _isUTC : true,
  2589. _l : lang,
  2590. _i : input,
  2591. _f : format,
  2592. _strict : strict
  2593. }).utc();
  2594. return m;
  2595. };
  2596. // creating with unix timestamp (in seconds)
  2597. moment.unix = function (input) {
  2598. return moment(input * 1000);
  2599. };
  2600. // duration
  2601. moment.duration = function (input, key) {
  2602. var isDuration = moment.isDuration(input),
  2603. isNumber = (typeof input === 'number'),
  2604. duration = (isDuration ? input._input : (isNumber ? {} : input)),
  2605. // matching against regexp is expensive, do it on demand
  2606. match = null,
  2607. sign,
  2608. ret,
  2609. parseIso,
  2610. timeEmpty,
  2611. dateTimeEmpty;
  2612. if (isNumber) {
  2613. if (key) {
  2614. duration[key] = input;
  2615. } else {
  2616. duration.milliseconds = input;
  2617. }
  2618. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  2619. sign = (match[1] === "-") ? -1 : 1;
  2620. duration = {
  2621. y: 0,
  2622. d: toInt(match[DATE]) * sign,
  2623. h: toInt(match[HOUR]) * sign,
  2624. m: toInt(match[MINUTE]) * sign,
  2625. s: toInt(match[SECOND]) * sign,
  2626. ms: toInt(match[MILLISECOND]) * sign
  2627. };
  2628. } else if (!!(match = isoDurationRegex.exec(input))) {
  2629. sign = (match[1] === "-") ? -1 : 1;
  2630. parseIso = function (inp) {
  2631. // We'd normally use ~~inp for this, but unfortunately it also
  2632. // converts floats to ints.
  2633. // inp may be undefined, so careful calling replace on it.
  2634. var res = inp && parseFloat(inp.replace(',', '.'));
  2635. // apply sign while we're at it
  2636. return (isNaN(res) ? 0 : res) * sign;
  2637. };
  2638. duration = {
  2639. y: parseIso(match[2]),
  2640. M: parseIso(match[3]),
  2641. d: parseIso(match[4]),
  2642. h: parseIso(match[5]),
  2643. m: parseIso(match[6]),
  2644. s: parseIso(match[7]),
  2645. w: parseIso(match[8])
  2646. };
  2647. }
  2648. ret = new Duration(duration);
  2649. if (isDuration && input.hasOwnProperty('_lang')) {
  2650. ret._lang = input._lang;
  2651. }
  2652. return ret;
  2653. };
  2654. // version number
  2655. moment.version = VERSION;
  2656. // default format
  2657. moment.defaultFormat = isoFormat;
  2658. // This function will be called whenever a moment is mutated.
  2659. // It is intended to keep the offset in sync with the timezone.
  2660. moment.updateOffset = function () {};
  2661. // This function will load languages and then set the global language. If
  2662. // no arguments are passed in, it will simply return the current global
  2663. // language key.
  2664. moment.lang = function (key, values) {
  2665. var r;
  2666. if (!key) {
  2667. return moment.fn._lang._abbr;
  2668. }
  2669. if (values) {
  2670. loadLang(normalizeLanguage(key), values);
  2671. } else if (values === null) {
  2672. unloadLang(key);
  2673. key = 'en';
  2674. } else if (!languages[key]) {
  2675. getLangDefinition(key);
  2676. }
  2677. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  2678. return r._abbr;
  2679. };
  2680. // returns language data
  2681. moment.langData = function (key) {
  2682. if (key && key._lang && key._lang._abbr) {
  2683. key = key._lang._abbr;
  2684. }
  2685. return getLangDefinition(key);
  2686. };
  2687. // compare moment object
  2688. moment.isMoment = function (obj) {
  2689. return obj instanceof Moment;
  2690. };
  2691. // for typechecking Duration objects
  2692. moment.isDuration = function (obj) {
  2693. return obj instanceof Duration;
  2694. };
  2695. for (i = lists.length - 1; i >= 0; --i) {
  2696. makeList(lists[i]);
  2697. }
  2698. moment.normalizeUnits = function (units) {
  2699. return normalizeUnits(units);
  2700. };
  2701. moment.invalid = function (flags) {
  2702. var m = moment.utc(NaN);
  2703. if (flags != null) {
  2704. extend(m._pf, flags);
  2705. }
  2706. else {
  2707. m._pf.userInvalidated = true;
  2708. }
  2709. return m;
  2710. };
  2711. moment.parseZone = function (input) {
  2712. return moment(input).parseZone();
  2713. };
  2714. /************************************
  2715. Moment Prototype
  2716. ************************************/
  2717. extend(moment.fn = Moment.prototype, {
  2718. clone : function () {
  2719. return moment(this);
  2720. },
  2721. valueOf : function () {
  2722. return +this._d + ((this._offset || 0) * 60000);
  2723. },
  2724. unix : function () {
  2725. return Math.floor(+this / 1000);
  2726. },
  2727. toString : function () {
  2728. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  2729. },
  2730. toDate : function () {
  2731. return this._offset ? new Date(+this) : this._d;
  2732. },
  2733. toISOString : function () {
  2734. return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  2735. },
  2736. toArray : function () {
  2737. var m = this;
  2738. return [
  2739. m.year(),
  2740. m.month(),
  2741. m.date(),
  2742. m.hours(),
  2743. m.minutes(),
  2744. m.seconds(),
  2745. m.milliseconds()
  2746. ];
  2747. },
  2748. isValid : function () {
  2749. return isValid(this);
  2750. },
  2751. isDSTShifted : function () {
  2752. if (this._a) {
  2753. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  2754. }
  2755. return false;
  2756. },
  2757. parsingFlags : function () {
  2758. return extend({}, this._pf);
  2759. },
  2760. invalidAt: function () {
  2761. return this._pf.overflow;
  2762. },
  2763. utc : function () {
  2764. return this.zone(0);
  2765. },
  2766. local : function () {
  2767. this.zone(0);
  2768. this._isUTC = false;
  2769. return this;
  2770. },
  2771. format : function (inputString) {
  2772. var output = formatMoment(this, inputString || moment.defaultFormat);
  2773. return this.lang().postformat(output);
  2774. },
  2775. add : function (input, val) {
  2776. var dur;
  2777. // switch args to support add('s', 1) and add(1, 's')
  2778. if (typeof input === 'string') {
  2779. dur = moment.duration(+val, input);
  2780. } else {
  2781. dur = moment.duration(input, val);
  2782. }
  2783. addOrSubtractDurationFromMoment(this, dur, 1);
  2784. return this;
  2785. },
  2786. subtract : function (input, val) {
  2787. var dur;
  2788. // switch args to support subtract('s', 1) and subtract(1, 's')
  2789. if (typeof input === 'string') {
  2790. dur = moment.duration(+val, input);
  2791. } else {
  2792. dur = moment.duration(input, val);
  2793. }
  2794. addOrSubtractDurationFromMoment(this, dur, -1);
  2795. return this;
  2796. },
  2797. diff : function (input, units, asFloat) {
  2798. var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(),
  2799. zoneDiff = (this.zone() - that.zone()) * 6e4,
  2800. diff, output;
  2801. units = normalizeUnits(units);
  2802. if (units === 'year' || units === 'month') {
  2803. // average number of days in the months in the given dates
  2804. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  2805. // difference in months
  2806. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  2807. // adjust by taking difference in days, average number of days
  2808. // and dst in the given months.
  2809. output += ((this - moment(this).startOf('month')) -
  2810. (that - moment(that).startOf('month'))) / diff;
  2811. // same as above but with zones, to negate all dst
  2812. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  2813. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  2814. if (units === 'year') {
  2815. output = output / 12;
  2816. }
  2817. } else {
  2818. diff = (this - that);
  2819. output = units === 'second' ? diff / 1e3 : // 1000
  2820. units === 'minute' ? diff / 6e4 : // 1000 * 60
  2821. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  2822. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  2823. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  2824. diff;
  2825. }
  2826. return asFloat ? output : absRound(output);
  2827. },
  2828. from : function (time, withoutSuffix) {
  2829. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  2830. },
  2831. fromNow : function (withoutSuffix) {
  2832. return this.from(moment(), withoutSuffix);
  2833. },
  2834. calendar : function () {
  2835. var diff = this.diff(moment().zone(this.zone()).startOf('day'), 'days', true),
  2836. format = diff < -6 ? 'sameElse' :
  2837. diff < -1 ? 'lastWeek' :
  2838. diff < 0 ? 'lastDay' :
  2839. diff < 1 ? 'sameDay' :
  2840. diff < 2 ? 'nextDay' :
  2841. diff < 7 ? 'nextWeek' : 'sameElse';
  2842. return this.format(this.lang().calendar(format, this));
  2843. },
  2844. isLeapYear : function () {
  2845. return isLeapYear(this.year());
  2846. },
  2847. isDST : function () {
  2848. return (this.zone() < this.clone().month(0).zone() ||
  2849. this.zone() < this.clone().month(5).zone());
  2850. },
  2851. day : function (input) {
  2852. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  2853. if (input != null) {
  2854. input = parseWeekday(input, this.lang());
  2855. return this.add({ d : input - day });
  2856. } else {
  2857. return day;
  2858. }
  2859. },
  2860. month : function (input) {
  2861. var utc = this._isUTC ? 'UTC' : '',
  2862. dayOfMonth;
  2863. if (input != null) {
  2864. if (typeof input === 'string') {
  2865. input = this.lang().monthsParse(input);
  2866. if (typeof input !== 'number') {
  2867. return this;
  2868. }
  2869. }
  2870. dayOfMonth = this.date();
  2871. this.date(1);
  2872. this._d['set' + utc + 'Month'](input);
  2873. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  2874. moment.updateOffset(this);
  2875. return this;
  2876. } else {
  2877. return this._d['get' + utc + 'Month']();
  2878. }
  2879. },
  2880. startOf: function (units) {
  2881. units = normalizeUnits(units);
  2882. // the following switch intentionally omits break keywords
  2883. // to utilize falling through the cases.
  2884. switch (units) {
  2885. case 'year':
  2886. this.month(0);
  2887. /* falls through */
  2888. case 'month':
  2889. this.date(1);
  2890. /* falls through */
  2891. case 'week':
  2892. case 'isoWeek':
  2893. case 'day':
  2894. this.hours(0);
  2895. /* falls through */
  2896. case 'hour':
  2897. this.minutes(0);
  2898. /* falls through */
  2899. case 'minute':
  2900. this.seconds(0);
  2901. /* falls through */
  2902. case 'second':
  2903. this.milliseconds(0);
  2904. /* falls through */
  2905. }
  2906. // weeks are a special case
  2907. if (units === 'week') {
  2908. this.weekday(0);
  2909. } else if (units === 'isoWeek') {
  2910. this.isoWeekday(1);
  2911. }
  2912. return this;
  2913. },
  2914. endOf: function (units) {
  2915. units = normalizeUnits(units);
  2916. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  2917. },
  2918. isAfter: function (input, units) {
  2919. units = typeof units !== 'undefined' ? units : 'millisecond';
  2920. return +this.clone().startOf(units) > +moment(input).startOf(units);
  2921. },
  2922. isBefore: function (input, units) {
  2923. units = typeof units !== 'undefined' ? units : 'millisecond';
  2924. return +this.clone().startOf(units) < +moment(input).startOf(units);
  2925. },
  2926. isSame: function (input, units) {
  2927. units = typeof units !== 'undefined' ? units : 'millisecond';
  2928. return +this.clone().startOf(units) === +moment(input).startOf(units);
  2929. },
  2930. min: function (other) {
  2931. other = moment.apply(null, arguments);
  2932. return other < this ? this : other;
  2933. },
  2934. max: function (other) {
  2935. other = moment.apply(null, arguments);
  2936. return other > this ? this : other;
  2937. },
  2938. zone : function (input) {
  2939. var offset = this._offset || 0;
  2940. if (input != null) {
  2941. if (typeof input === "string") {
  2942. input = timezoneMinutesFromString(input);
  2943. }
  2944. if (Math.abs(input) < 16) {
  2945. input = input * 60;
  2946. }
  2947. this._offset = input;
  2948. this._isUTC = true;
  2949. if (offset !== input) {
  2950. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  2951. }
  2952. } else {
  2953. return this._isUTC ? offset : this._d.getTimezoneOffset();
  2954. }
  2955. return this;
  2956. },
  2957. zoneAbbr : function () {
  2958. return this._isUTC ? "UTC" : "";
  2959. },
  2960. zoneName : function () {
  2961. return this._isUTC ? "Coordinated Universal Time" : "";
  2962. },
  2963. parseZone : function () {
  2964. if (typeof this._i === 'string') {
  2965. this.zone(this._i);
  2966. }
  2967. return this;
  2968. },
  2969. hasAlignedHourOffset : function (input) {
  2970. if (!input) {
  2971. input = 0;
  2972. }
  2973. else {
  2974. input = moment(input).zone();
  2975. }
  2976. return (this.zone() - input) % 60 === 0;
  2977. },
  2978. daysInMonth : function () {
  2979. return daysInMonth(this.year(), this.month());
  2980. },
  2981. dayOfYear : function (input) {
  2982. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  2983. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  2984. },
  2985. weekYear : function (input) {
  2986. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  2987. return input == null ? year : this.add("y", (input - year));
  2988. },
  2989. isoWeekYear : function (input) {
  2990. var year = weekOfYear(this, 1, 4).year;
  2991. return input == null ? year : this.add("y", (input - year));
  2992. },
  2993. week : function (input) {
  2994. var week = this.lang().week(this);
  2995. return input == null ? week : this.add("d", (input - week) * 7);
  2996. },
  2997. isoWeek : function (input) {
  2998. var week = weekOfYear(this, 1, 4).week;
  2999. return input == null ? week : this.add("d", (input - week) * 7);
  3000. },
  3001. weekday : function (input) {
  3002. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  3003. return input == null ? weekday : this.add("d", input - weekday);
  3004. },
  3005. isoWeekday : function (input) {
  3006. // behaves the same as moment#day except
  3007. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  3008. // as a setter, sunday should belong to the previous week.
  3009. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  3010. },
  3011. get : function (units) {
  3012. units = normalizeUnits(units);
  3013. return this[units]();
  3014. },
  3015. set : function (units, value) {
  3016. units = normalizeUnits(units);
  3017. if (typeof this[units] === 'function') {
  3018. this[units](value);
  3019. }
  3020. return this;
  3021. },
  3022. // If passed a language key, it will set the language for this
  3023. // instance. Otherwise, it will return the language configuration
  3024. // variables for this instance.
  3025. lang : function (key) {
  3026. if (key === undefined) {
  3027. return this._lang;
  3028. } else {
  3029. this._lang = getLangDefinition(key);
  3030. return this;
  3031. }
  3032. }
  3033. });
  3034. // helper for adding shortcuts
  3035. function makeGetterAndSetter(name, key) {
  3036. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  3037. var utc = this._isUTC ? 'UTC' : '';
  3038. if (input != null) {
  3039. this._d['set' + utc + key](input);
  3040. moment.updateOffset(this);
  3041. return this;
  3042. } else {
  3043. return this._d['get' + utc + key]();
  3044. }
  3045. };
  3046. }
  3047. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  3048. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  3049. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  3050. }
  3051. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  3052. makeGetterAndSetter('year', 'FullYear');
  3053. // add plural methods
  3054. moment.fn.days = moment.fn.day;
  3055. moment.fn.months = moment.fn.month;
  3056. moment.fn.weeks = moment.fn.week;
  3057. moment.fn.isoWeeks = moment.fn.isoWeek;
  3058. // add aliased format methods
  3059. moment.fn.toJSON = moment.fn.toISOString;
  3060. /************************************
  3061. Duration Prototype
  3062. ************************************/
  3063. extend(moment.duration.fn = Duration.prototype, {
  3064. _bubble : function () {
  3065. var milliseconds = this._milliseconds,
  3066. days = this._days,
  3067. months = this._months,
  3068. data = this._data,
  3069. seconds, minutes, hours, years;
  3070. // The following code bubbles up values, see the tests for
  3071. // examples of what that means.
  3072. data.milliseconds = milliseconds % 1000;
  3073. seconds = absRound(milliseconds / 1000);
  3074. data.seconds = seconds % 60;
  3075. minutes = absRound(seconds / 60);
  3076. data.minutes = minutes % 60;
  3077. hours = absRound(minutes / 60);
  3078. data.hours = hours % 24;
  3079. days += absRound(hours / 24);
  3080. data.days = days % 30;
  3081. months += absRound(days / 30);
  3082. data.months = months % 12;
  3083. years = absRound(months / 12);
  3084. data.years = years;
  3085. },
  3086. weeks : function () {
  3087. return absRound(this.days() / 7);
  3088. },
  3089. valueOf : function () {
  3090. return this._milliseconds +
  3091. this._days * 864e5 +
  3092. (this._months % 12) * 2592e6 +
  3093. toInt(this._months / 12) * 31536e6;
  3094. },
  3095. humanize : function (withSuffix) {
  3096. var difference = +this,
  3097. output = relativeTime(difference, !withSuffix, this.lang());
  3098. if (withSuffix) {
  3099. output = this.lang().pastFuture(difference, output);
  3100. }
  3101. return this.lang().postformat(output);
  3102. },
  3103. add : function (input, val) {
  3104. // supports only 2.0-style add(1, 's') or add(moment)
  3105. var dur = moment.duration(input, val);
  3106. this._milliseconds += dur._milliseconds;
  3107. this._days += dur._days;
  3108. this._months += dur._months;
  3109. this._bubble();
  3110. return this;
  3111. },
  3112. subtract : function (input, val) {
  3113. var dur = moment.duration(input, val);
  3114. this._milliseconds -= dur._milliseconds;
  3115. this._days -= dur._days;
  3116. this._months -= dur._months;
  3117. this._bubble();
  3118. return this;
  3119. },
  3120. get : function (units) {
  3121. units = normalizeUnits(units);
  3122. return this[units.toLowerCase() + 's']();
  3123. },
  3124. as : function (units) {
  3125. units = normalizeUnits(units);
  3126. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  3127. },
  3128. lang : moment.fn.lang,
  3129. toIsoString : function () {
  3130. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  3131. var years = Math.abs(this.years()),
  3132. months = Math.abs(this.months()),
  3133. days = Math.abs(this.days()),
  3134. hours = Math.abs(this.hours()),
  3135. minutes = Math.abs(this.minutes()),
  3136. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  3137. if (!this.asSeconds()) {
  3138. // this is the same as C#'s (Noda) and python (isodate)...
  3139. // but not other JS (goog.date)
  3140. return 'P0D';
  3141. }
  3142. return (this.asSeconds() < 0 ? '-' : '') +
  3143. 'P' +
  3144. (years ? years + 'Y' : '') +
  3145. (months ? months + 'M' : '') +
  3146. (days ? days + 'D' : '') +
  3147. ((hours || minutes || seconds) ? 'T' : '') +
  3148. (hours ? hours + 'H' : '') +
  3149. (minutes ? minutes + 'M' : '') +
  3150. (seconds ? seconds + 'S' : '');
  3151. }
  3152. });
  3153. function makeDurationGetter(name) {
  3154. moment.duration.fn[name] = function () {
  3155. return this._data[name];
  3156. };
  3157. }
  3158. function makeDurationAsGetter(name, factor) {
  3159. moment.duration.fn['as' + name] = function () {
  3160. return +this / factor;
  3161. };
  3162. }
  3163. for (i in unitMillisecondFactors) {
  3164. if (unitMillisecondFactors.hasOwnProperty(i)) {
  3165. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  3166. makeDurationGetter(i.toLowerCase());
  3167. }
  3168. }
  3169. makeDurationAsGetter('Weeks', 6048e5);
  3170. moment.duration.fn.asMonths = function () {
  3171. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  3172. };
  3173. /************************************
  3174. Default Lang
  3175. ************************************/
  3176. // Set default language, other languages will inherit from English.
  3177. moment.lang('en', {
  3178. ordinal : function (number) {
  3179. var b = number % 10,
  3180. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  3181. (b === 1) ? 'st' :
  3182. (b === 2) ? 'nd' :
  3183. (b === 3) ? 'rd' : 'th';
  3184. return number + output;
  3185. }
  3186. });
  3187. /* EMBED_LANGUAGES */
  3188. /************************************
  3189. Exposing Moment
  3190. ************************************/
  3191. function makeGlobal(deprecate) {
  3192. var warned = false, local_moment = moment;
  3193. /*global ender:false */
  3194. if (typeof ender !== 'undefined') {
  3195. return;
  3196. }
  3197. // here, `this` means `window` in the browser, or `global` on the server
  3198. // add `moment` as a global object via a string identifier,
  3199. // for Closure Compiler "advanced" mode
  3200. if (deprecate) {
  3201. this.moment = function () {
  3202. if (!warned && console && console.warn) {
  3203. warned = true;
  3204. console.warn(
  3205. "Accessing Moment through the global scope is " +
  3206. "deprecated, and will be removed in an upcoming " +
  3207. "release.");
  3208. }
  3209. return local_moment.apply(null, arguments);
  3210. };
  3211. } else {
  3212. this['moment'] = moment;
  3213. }
  3214. }
  3215. // CommonJS module is defined
  3216. if (hasModule) {
  3217. module.exports = moment;
  3218. makeGlobal(true);
  3219. } else if (typeof define === "function" && define.amd) {
  3220. define("moment", function (require, exports, module) {
  3221. if (module.config().noGlobal !== true) {
  3222. // If user provided noGlobal, he is aware of global
  3223. makeGlobal(module.config().noGlobal === undefined);
  3224. }
  3225. return moment;
  3226. });
  3227. } else {
  3228. makeGlobal();
  3229. }
  3230. }).call(this);
  3231. },{}],3:[function(require,module,exports){
  3232. /**
  3233. * vis.js module imports
  3234. */
  3235. // Try to load dependencies from the global window object.
  3236. // If not available there, load via require.
  3237. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  3238. var Hammer;
  3239. if (typeof window !== 'undefined') {
  3240. // load hammer.js only when running in a browser (where window is available)
  3241. Hammer = window['Hammer'] || require('hammerjs');
  3242. }
  3243. else {
  3244. Hammer = function () {
  3245. throw Error('hammer.js is only available in a browser, not in node.js.');
  3246. }
  3247. }
  3248. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  3249. // it here in that case.
  3250. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  3251. if(!Array.prototype.indexOf) {
  3252. Array.prototype.indexOf = function(obj){
  3253. for(var i = 0; i < this.length; i++){
  3254. if(this[i] == obj){
  3255. return i;
  3256. }
  3257. }
  3258. return -1;
  3259. };
  3260. try {
  3261. console.log("Warning: Ancient browser detected. Please update your browser");
  3262. }
  3263. catch (err) {
  3264. }
  3265. }
  3266. // Internet Explorer 8 and older does not support Array.forEach, so we define
  3267. // it here in that case.
  3268. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  3269. if (!Array.prototype.forEach) {
  3270. Array.prototype.forEach = function(fn, scope) {
  3271. for(var i = 0, len = this.length; i < len; ++i) {
  3272. fn.call(scope || this, this[i], i, this);
  3273. }
  3274. }
  3275. }
  3276. // Internet Explorer 8 and older does not support Array.map, so we define it
  3277. // here in that case.
  3278. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  3279. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  3280. // Reference: http://es5.github.com/#x15.4.4.19
  3281. if (!Array.prototype.map) {
  3282. Array.prototype.map = function(callback, thisArg) {
  3283. var T, A, k;
  3284. if (this == null) {
  3285. throw new TypeError(" this is null or not defined");
  3286. }
  3287. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  3288. var O = Object(this);
  3289. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  3290. // 3. Let len be ToUint32(lenValue).
  3291. var len = O.length >>> 0;
  3292. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  3293. // See: http://es5.github.com/#x9.11
  3294. if (typeof callback !== "function") {
  3295. throw new TypeError(callback + " is not a function");
  3296. }
  3297. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  3298. if (thisArg) {
  3299. T = thisArg;
  3300. }
  3301. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  3302. // the standard built-in constructor with that name and len is the value of len.
  3303. A = new Array(len);
  3304. // 7. Let k be 0
  3305. k = 0;
  3306. // 8. Repeat, while k < len
  3307. while(k < len) {
  3308. var kValue, mappedValue;
  3309. // a. Let Pk be ToString(k).
  3310. // This is implicit for LHS operands of the in operator
  3311. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  3312. // This step can be combined with c
  3313. // c. If kPresent is true, then
  3314. if (k in O) {
  3315. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  3316. kValue = O[ k ];
  3317. // ii. Let mappedValue be the result of calling the Call internal method of callback
  3318. // with T as the this value and argument list containing kValue, k, and O.
  3319. mappedValue = callback.call(T, kValue, k, O);
  3320. // iii. Call the DefineOwnProperty internal method of A with arguments
  3321. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  3322. // and false.
  3323. // In browsers that support Object.defineProperty, use the following:
  3324. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  3325. // For best browser support, use the following:
  3326. A[ k ] = mappedValue;
  3327. }
  3328. // d. Increase k by 1.
  3329. k++;
  3330. }
  3331. // 9. return A
  3332. return A;
  3333. };
  3334. }
  3335. // Internet Explorer 8 and older does not support Array.filter, so we define it
  3336. // here in that case.
  3337. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  3338. if (!Array.prototype.filter) {
  3339. Array.prototype.filter = function(fun /*, thisp */) {
  3340. "use strict";
  3341. if (this == null) {
  3342. throw new TypeError();
  3343. }
  3344. var t = Object(this);
  3345. var len = t.length >>> 0;
  3346. if (typeof fun != "function") {
  3347. throw new TypeError();
  3348. }
  3349. var res = [];
  3350. var thisp = arguments[1];
  3351. for (var i = 0; i < len; i++) {
  3352. if (i in t) {
  3353. var val = t[i]; // in case fun mutates this
  3354. if (fun.call(thisp, val, i, t))
  3355. res.push(val);
  3356. }
  3357. }
  3358. return res;
  3359. };
  3360. }
  3361. // Internet Explorer 8 and older does not support Object.keys, so we define it
  3362. // here in that case.
  3363. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  3364. if (!Object.keys) {
  3365. Object.keys = (function () {
  3366. var hasOwnProperty = Object.prototype.hasOwnProperty,
  3367. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  3368. dontEnums = [
  3369. 'toString',
  3370. 'toLocaleString',
  3371. 'valueOf',
  3372. 'hasOwnProperty',
  3373. 'isPrototypeOf',
  3374. 'propertyIsEnumerable',
  3375. 'constructor'
  3376. ],
  3377. dontEnumsLength = dontEnums.length;
  3378. return function (obj) {
  3379. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  3380. throw new TypeError('Object.keys called on non-object');
  3381. }
  3382. var result = [];
  3383. for (var prop in obj) {
  3384. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  3385. }
  3386. if (hasDontEnumBug) {
  3387. for (var i=0; i < dontEnumsLength; i++) {
  3388. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  3389. }
  3390. }
  3391. return result;
  3392. }
  3393. })()
  3394. }
  3395. // Internet Explorer 8 and older does not support Array.isArray,
  3396. // so we define it here in that case.
  3397. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  3398. if(!Array.isArray) {
  3399. Array.isArray = function (vArg) {
  3400. return Object.prototype.toString.call(vArg) === "[object Array]";
  3401. };
  3402. }
  3403. // Internet Explorer 8 and older does not support Function.bind,
  3404. // so we define it here in that case.
  3405. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  3406. if (!Function.prototype.bind) {
  3407. Function.prototype.bind = function (oThis) {
  3408. if (typeof this !== "function") {
  3409. // closest thing possible to the ECMAScript 5 internal IsCallable function
  3410. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  3411. }
  3412. var aArgs = Array.prototype.slice.call(arguments, 1),
  3413. fToBind = this,
  3414. fNOP = function () {},
  3415. fBound = function () {
  3416. return fToBind.apply(this instanceof fNOP && oThis
  3417. ? this
  3418. : oThis,
  3419. aArgs.concat(Array.prototype.slice.call(arguments)));
  3420. };
  3421. fNOP.prototype = this.prototype;
  3422. fBound.prototype = new fNOP();
  3423. return fBound;
  3424. };
  3425. }
  3426. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  3427. if (!Object.create) {
  3428. Object.create = function (o) {
  3429. if (arguments.length > 1) {
  3430. throw new Error('Object.create implementation only accepts the first parameter.');
  3431. }
  3432. function F() {}
  3433. F.prototype = o;
  3434. return new F();
  3435. };
  3436. }
  3437. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  3438. if (!Function.prototype.bind) {
  3439. Function.prototype.bind = function (oThis) {
  3440. if (typeof this !== "function") {
  3441. // closest thing possible to the ECMAScript 5 internal IsCallable function
  3442. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  3443. }
  3444. var aArgs = Array.prototype.slice.call(arguments, 1),
  3445. fToBind = this,
  3446. fNOP = function () {},
  3447. fBound = function () {
  3448. return fToBind.apply(this instanceof fNOP && oThis
  3449. ? this
  3450. : oThis,
  3451. aArgs.concat(Array.prototype.slice.call(arguments)));
  3452. };
  3453. fNOP.prototype = this.prototype;
  3454. fBound.prototype = new fNOP();
  3455. return fBound;
  3456. };
  3457. }
  3458. /**
  3459. * utility functions
  3460. */
  3461. var util = {};
  3462. /**
  3463. * Test whether given object is a number
  3464. * @param {*} object
  3465. * @return {Boolean} isNumber
  3466. */
  3467. util.isNumber = function isNumber(object) {
  3468. return (object instanceof Number || typeof object == 'number');
  3469. };
  3470. /**
  3471. * Test whether given object is a string
  3472. * @param {*} object
  3473. * @return {Boolean} isString
  3474. */
  3475. util.isString = function isString(object) {
  3476. return (object instanceof String || typeof object == 'string');
  3477. };
  3478. /**
  3479. * Test whether given object is a Date, or a String containing a Date
  3480. * @param {Date | String} object
  3481. * @return {Boolean} isDate
  3482. */
  3483. util.isDate = function isDate(object) {
  3484. if (object instanceof Date) {
  3485. return true;
  3486. }
  3487. else if (util.isString(object)) {
  3488. // test whether this string contains a date
  3489. var match = ASPDateRegex.exec(object);
  3490. if (match) {
  3491. return true;
  3492. }
  3493. else if (!isNaN(Date.parse(object))) {
  3494. return true;
  3495. }
  3496. }
  3497. return false;
  3498. };
  3499. /**
  3500. * Test whether given object is an instance of google.visualization.DataTable
  3501. * @param {*} object
  3502. * @return {Boolean} isDataTable
  3503. */
  3504. util.isDataTable = function isDataTable(object) {
  3505. return (typeof (google) !== 'undefined') &&
  3506. (google.visualization) &&
  3507. (google.visualization.DataTable) &&
  3508. (object instanceof google.visualization.DataTable);
  3509. };
  3510. /**
  3511. * Create a semi UUID
  3512. * source: http://stackoverflow.com/a/105074/1262753
  3513. * @return {String} uuid
  3514. */
  3515. util.randomUUID = function randomUUID () {
  3516. var S4 = function () {
  3517. return Math.floor(
  3518. Math.random() * 0x10000 /* 65536 */
  3519. ).toString(16);
  3520. };
  3521. return (
  3522. S4() + S4() + '-' +
  3523. S4() + '-' +
  3524. S4() + '-' +
  3525. S4() + '-' +
  3526. S4() + S4() + S4()
  3527. );
  3528. };
  3529. /**
  3530. * Extend object a with the properties of object b or a series of objects
  3531. * Only properties with defined values are copied
  3532. * @param {Object} a
  3533. * @param {... Object} b
  3534. * @return {Object} a
  3535. */
  3536. util.extend = function (a, b) {
  3537. for (var i = 1, len = arguments.length; i < len; i++) {
  3538. var other = arguments[i];
  3539. for (var prop in other) {
  3540. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  3541. a[prop] = other[prop];
  3542. }
  3543. }
  3544. }
  3545. return a;
  3546. };
  3547. /**
  3548. * Convert an object to another type
  3549. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  3550. * @param {String | undefined} type Name of the type. Available types:
  3551. * 'Boolean', 'Number', 'String',
  3552. * 'Date', 'Moment', ISODate', 'ASPDate'.
  3553. * @return {*} object
  3554. * @throws Error
  3555. */
  3556. util.convert = function convert(object, type) {
  3557. var match;
  3558. if (object === undefined) {
  3559. return undefined;
  3560. }
  3561. if (object === null) {
  3562. return null;
  3563. }
  3564. if (!type) {
  3565. return object;
  3566. }
  3567. if (!(typeof type === 'string') && !(type instanceof String)) {
  3568. throw new Error('Type must be a string');
  3569. }
  3570. //noinspection FallthroughInSwitchStatementJS
  3571. switch (type) {
  3572. case 'boolean':
  3573. case 'Boolean':
  3574. return Boolean(object);
  3575. case 'number':
  3576. case 'Number':
  3577. return Number(object.valueOf());
  3578. case 'string':
  3579. case 'String':
  3580. return String(object);
  3581. case 'Date':
  3582. if (util.isNumber(object)) {
  3583. return new Date(object);
  3584. }
  3585. if (object instanceof Date) {
  3586. return new Date(object.valueOf());
  3587. }
  3588. else if (moment.isMoment(object)) {
  3589. return new Date(object.valueOf());
  3590. }
  3591. if (util.isString(object)) {
  3592. match = ASPDateRegex.exec(object);
  3593. if (match) {
  3594. // object is an ASP date
  3595. return new Date(Number(match[1])); // parse number
  3596. }
  3597. else {
  3598. return moment(object).toDate(); // parse string
  3599. }
  3600. }
  3601. else {
  3602. throw new Error(
  3603. 'Cannot convert object of type ' + util.getType(object) +
  3604. ' to type Date');
  3605. }
  3606. case 'Moment':
  3607. if (util.isNumber(object)) {
  3608. return moment(object);
  3609. }
  3610. if (object instanceof Date) {
  3611. return moment(object.valueOf());
  3612. }
  3613. else if (moment.isMoment(object)) {
  3614. return moment(object);
  3615. }
  3616. if (util.isString(object)) {
  3617. match = ASPDateRegex.exec(object);
  3618. if (match) {
  3619. // object is an ASP date
  3620. return moment(Number(match[1])); // parse number
  3621. }
  3622. else {
  3623. return moment(object); // parse string
  3624. }
  3625. }
  3626. else {
  3627. throw new Error(
  3628. 'Cannot convert object of type ' + util.getType(object) +
  3629. ' to type Date');
  3630. }
  3631. case 'ISODate':
  3632. if (util.isNumber(object)) {
  3633. return new Date(object);
  3634. }
  3635. else if (object instanceof Date) {
  3636. return object.toISOString();
  3637. }
  3638. else if (moment.isMoment(object)) {
  3639. return object.toDate().toISOString();
  3640. }
  3641. else if (util.isString(object)) {
  3642. match = ASPDateRegex.exec(object);
  3643. if (match) {
  3644. // object is an ASP date
  3645. return new Date(Number(match[1])).toISOString(); // parse number
  3646. }
  3647. else {
  3648. return new Date(object).toISOString(); // parse string
  3649. }
  3650. }
  3651. else {
  3652. throw new Error(
  3653. 'Cannot convert object of type ' + util.getType(object) +
  3654. ' to type ISODate');
  3655. }
  3656. case 'ASPDate':
  3657. if (util.isNumber(object)) {
  3658. return '/Date(' + object + ')/';
  3659. }
  3660. else if (object instanceof Date) {
  3661. return '/Date(' + object.valueOf() + ')/';
  3662. }
  3663. else if (util.isString(object)) {
  3664. match = ASPDateRegex.exec(object);
  3665. var value;
  3666. if (match) {
  3667. // object is an ASP date
  3668. value = new Date(Number(match[1])).valueOf(); // parse number
  3669. }
  3670. else {
  3671. value = new Date(object).valueOf(); // parse string
  3672. }
  3673. return '/Date(' + value + ')/';
  3674. }
  3675. else {
  3676. throw new Error(
  3677. 'Cannot convert object of type ' + util.getType(object) +
  3678. ' to type ASPDate');
  3679. }
  3680. default:
  3681. throw new Error('Cannot convert object of type ' + util.getType(object) +
  3682. ' to type "' + type + '"');
  3683. }
  3684. };
  3685. // parse ASP.Net Date pattern,
  3686. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  3687. // code from http://momentjs.com/
  3688. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  3689. /**
  3690. * Get the type of an object, for example util.getType([]) returns 'Array'
  3691. * @param {*} object
  3692. * @return {String} type
  3693. */
  3694. util.getType = function getType(object) {
  3695. var type = typeof object;
  3696. if (type == 'object') {
  3697. if (object == null) {
  3698. return 'null';
  3699. }
  3700. if (object instanceof Boolean) {
  3701. return 'Boolean';
  3702. }
  3703. if (object instanceof Number) {
  3704. return 'Number';
  3705. }
  3706. if (object instanceof String) {
  3707. return 'String';
  3708. }
  3709. if (object instanceof Array) {
  3710. return 'Array';
  3711. }
  3712. if (object instanceof Date) {
  3713. return 'Date';
  3714. }
  3715. return 'Object';
  3716. }
  3717. else if (type == 'number') {
  3718. return 'Number';
  3719. }
  3720. else if (type == 'boolean') {
  3721. return 'Boolean';
  3722. }
  3723. else if (type == 'string') {
  3724. return 'String';
  3725. }
  3726. return type;
  3727. };
  3728. /**
  3729. * Retrieve the absolute left value of a DOM element
  3730. * @param {Element} elem A dom element, for example a div
  3731. * @return {number} left The absolute left position of this element
  3732. * in the browser page.
  3733. */
  3734. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  3735. var doc = document.documentElement;
  3736. var body = document.body;
  3737. var left = elem.offsetLeft;
  3738. var e = elem.offsetParent;
  3739. while (e != null && e != body && e != doc) {
  3740. left += e.offsetLeft;
  3741. left -= e.scrollLeft;
  3742. e = e.offsetParent;
  3743. }
  3744. return left;
  3745. };
  3746. /**
  3747. * Retrieve the absolute top value of a DOM element
  3748. * @param {Element} elem A dom element, for example a div
  3749. * @return {number} top The absolute top position of this element
  3750. * in the browser page.
  3751. */
  3752. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  3753. var doc = document.documentElement;
  3754. var body = document.body;
  3755. var top = elem.offsetTop;
  3756. var e = elem.offsetParent;
  3757. while (e != null && e != body && e != doc) {
  3758. top += e.offsetTop;
  3759. top -= e.scrollTop;
  3760. e = e.offsetParent;
  3761. }
  3762. return top;
  3763. };
  3764. /**
  3765. * Get the absolute, vertical mouse position from an event.
  3766. * @param {Event} event
  3767. * @return {Number} pageY
  3768. */
  3769. util.getPageY = function getPageY (event) {
  3770. if ('pageY' in event) {
  3771. return event.pageY;
  3772. }
  3773. else {
  3774. var clientY;
  3775. if (('targetTouches' in event) && event.targetTouches.length) {
  3776. clientY = event.targetTouches[0].clientY;
  3777. }
  3778. else {
  3779. clientY = event.clientY;
  3780. }
  3781. var doc = document.documentElement;
  3782. var body = document.body;
  3783. return clientY +
  3784. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  3785. ( doc && doc.clientTop || body && body.clientTop || 0 );
  3786. }
  3787. };
  3788. /**
  3789. * Get the absolute, horizontal mouse position from an event.
  3790. * @param {Event} event
  3791. * @return {Number} pageX
  3792. */
  3793. util.getPageX = function getPageX (event) {
  3794. if ('pageY' in event) {
  3795. return event.pageX;
  3796. }
  3797. else {
  3798. var clientX;
  3799. if (('targetTouches' in event) && event.targetTouches.length) {
  3800. clientX = event.targetTouches[0].clientX;
  3801. }
  3802. else {
  3803. clientX = event.clientX;
  3804. }
  3805. var doc = document.documentElement;
  3806. var body = document.body;
  3807. return clientX +
  3808. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  3809. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  3810. }
  3811. };
  3812. /**
  3813. * add a className to the given elements style
  3814. * @param {Element} elem
  3815. * @param {String} className
  3816. */
  3817. util.addClassName = function addClassName(elem, className) {
  3818. var classes = elem.className.split(' ');
  3819. if (classes.indexOf(className) == -1) {
  3820. classes.push(className); // add the class to the array
  3821. elem.className = classes.join(' ');
  3822. }
  3823. };
  3824. /**
  3825. * add a className to the given elements style
  3826. * @param {Element} elem
  3827. * @param {String} className
  3828. */
  3829. util.removeClassName = function removeClassname(elem, className) {
  3830. var classes = elem.className.split(' ');
  3831. var index = classes.indexOf(className);
  3832. if (index != -1) {
  3833. classes.splice(index, 1); // remove the class from the array
  3834. elem.className = classes.join(' ');
  3835. }
  3836. };
  3837. /**
  3838. * For each method for both arrays and objects.
  3839. * In case of an array, the built-in Array.forEach() is applied.
  3840. * In case of an Object, the method loops over all properties of the object.
  3841. * @param {Object | Array} object An Object or Array
  3842. * @param {function} callback Callback method, called for each item in
  3843. * the object or array with three parameters:
  3844. * callback(value, index, object)
  3845. */
  3846. util.forEach = function forEach (object, callback) {
  3847. var i,
  3848. len;
  3849. if (object instanceof Array) {
  3850. // array
  3851. for (i = 0, len = object.length; i < len; i++) {
  3852. callback(object[i], i, object);
  3853. }
  3854. }
  3855. else {
  3856. // object
  3857. for (i in object) {
  3858. if (object.hasOwnProperty(i)) {
  3859. callback(object[i], i, object);
  3860. }
  3861. }
  3862. }
  3863. };
  3864. /**
  3865. * Update a property in an object
  3866. * @param {Object} object
  3867. * @param {String} key
  3868. * @param {*} value
  3869. * @return {Boolean} changed
  3870. */
  3871. util.updateProperty = function updateProp (object, key, value) {
  3872. if (object[key] !== value) {
  3873. object[key] = value;
  3874. return true;
  3875. }
  3876. else {
  3877. return false;
  3878. }
  3879. };
  3880. /**
  3881. * Add and event listener. Works for all browsers
  3882. * @param {Element} element An html element
  3883. * @param {string} action The action, for example "click",
  3884. * without the prefix "on"
  3885. * @param {function} listener The callback function to be executed
  3886. * @param {boolean} [useCapture]
  3887. */
  3888. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  3889. if (element.addEventListener) {
  3890. if (useCapture === undefined)
  3891. useCapture = false;
  3892. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  3893. action = "DOMMouseScroll"; // For Firefox
  3894. }
  3895. element.addEventListener(action, listener, useCapture);
  3896. } else {
  3897. element.attachEvent("on" + action, listener); // IE browsers
  3898. }
  3899. };
  3900. /**
  3901. * Remove an event listener from an element
  3902. * @param {Element} element An html dom element
  3903. * @param {string} action The name of the event, for example "mousedown"
  3904. * @param {function} listener The listener function
  3905. * @param {boolean} [useCapture]
  3906. */
  3907. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  3908. if (element.removeEventListener) {
  3909. // non-IE browsers
  3910. if (useCapture === undefined)
  3911. useCapture = false;
  3912. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  3913. action = "DOMMouseScroll"; // For Firefox
  3914. }
  3915. element.removeEventListener(action, listener, useCapture);
  3916. } else {
  3917. // IE browsers
  3918. element.detachEvent("on" + action, listener);
  3919. }
  3920. };
  3921. /**
  3922. * Get HTML element which is the target of the event
  3923. * @param {Event} event
  3924. * @return {Element} target element
  3925. */
  3926. util.getTarget = function getTarget(event) {
  3927. // code from http://www.quirksmode.org/js/events_properties.html
  3928. if (!event) {
  3929. event = window.event;
  3930. }
  3931. var target;
  3932. if (event.target) {
  3933. target = event.target;
  3934. }
  3935. else if (event.srcElement) {
  3936. target = event.srcElement;
  3937. }
  3938. if (target.nodeType != undefined && target.nodeType == 3) {
  3939. // defeat Safari bug
  3940. target = target.parentNode;
  3941. }
  3942. return target;
  3943. };
  3944. /**
  3945. * Stop event propagation
  3946. */
  3947. util.stopPropagation = function stopPropagation(event) {
  3948. if (!event)
  3949. event = window.event;
  3950. if (event.stopPropagation) {
  3951. event.stopPropagation(); // non-IE browsers
  3952. }
  3953. else {
  3954. event.cancelBubble = true; // IE browsers
  3955. }
  3956. };
  3957. /**
  3958. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  3959. */
  3960. util.preventDefault = function preventDefault (event) {
  3961. if (!event)
  3962. event = window.event;
  3963. if (event.preventDefault) {
  3964. event.preventDefault(); // non-IE browsers
  3965. }
  3966. else {
  3967. event.returnValue = false; // IE browsers
  3968. }
  3969. };
  3970. util.option = {};
  3971. /**
  3972. * Convert a value into a boolean
  3973. * @param {Boolean | function | undefined} value
  3974. * @param {Boolean} [defaultValue]
  3975. * @returns {Boolean} bool
  3976. */
  3977. util.option.asBoolean = function (value, defaultValue) {
  3978. if (typeof value == 'function') {
  3979. value = value();
  3980. }
  3981. if (value != null) {
  3982. return (value != false);
  3983. }
  3984. return defaultValue || null;
  3985. };
  3986. /**
  3987. * Convert a value into a number
  3988. * @param {Boolean | function | undefined} value
  3989. * @param {Number} [defaultValue]
  3990. * @returns {Number} number
  3991. */
  3992. util.option.asNumber = function (value, defaultValue) {
  3993. if (typeof value == 'function') {
  3994. value = value();
  3995. }
  3996. if (value != null) {
  3997. return Number(value) || defaultValue || null;
  3998. }
  3999. return defaultValue || null;
  4000. };
  4001. /**
  4002. * Convert a value into a string
  4003. * @param {String | function | undefined} value
  4004. * @param {String} [defaultValue]
  4005. * @returns {String} str
  4006. */
  4007. util.option.asString = function (value, defaultValue) {
  4008. if (typeof value == 'function') {
  4009. value = value();
  4010. }
  4011. if (value != null) {
  4012. return String(value);
  4013. }
  4014. return defaultValue || null;
  4015. };
  4016. /**
  4017. * Convert a size or location into a string with pixels or a percentage
  4018. * @param {String | Number | function | undefined} value
  4019. * @param {String} [defaultValue]
  4020. * @returns {String} size
  4021. */
  4022. util.option.asSize = function (value, defaultValue) {
  4023. if (typeof value == 'function') {
  4024. value = value();
  4025. }
  4026. if (util.isString(value)) {
  4027. return value;
  4028. }
  4029. else if (util.isNumber(value)) {
  4030. return value + 'px';
  4031. }
  4032. else {
  4033. return defaultValue || null;
  4034. }
  4035. };
  4036. /**
  4037. * Convert a value into a DOM element
  4038. * @param {HTMLElement | function | undefined} value
  4039. * @param {HTMLElement} [defaultValue]
  4040. * @returns {HTMLElement | null} dom
  4041. */
  4042. util.option.asElement = function (value, defaultValue) {
  4043. if (typeof value == 'function') {
  4044. value = value();
  4045. }
  4046. return value || defaultValue || null;
  4047. };
  4048. /**
  4049. * load css from text
  4050. * @param {String} css Text containing css
  4051. */
  4052. util.loadCss = function (css) {
  4053. if (typeof document === 'undefined') {
  4054. return;
  4055. }
  4056. // get the script location, and built the css file name from the js file name
  4057. // http://stackoverflow.com/a/2161748/1262753
  4058. // var scripts = document.getElementsByTagName('script');
  4059. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  4060. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  4061. // inject css
  4062. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  4063. var style = document.createElement('style');
  4064. style.type = 'text/css';
  4065. if (style.styleSheet){
  4066. style.styleSheet.cssText = css;
  4067. } else {
  4068. style.appendChild(document.createTextNode(css));
  4069. }
  4070. document.getElementsByTagName('head')[0].appendChild(style);
  4071. };
  4072. /**
  4073. * Event listener (singleton)
  4074. */
  4075. // TODO: replace usage of the event listener for the EventBus
  4076. var events = {
  4077. 'listeners': [],
  4078. /**
  4079. * Find a single listener by its object
  4080. * @param {Object} object
  4081. * @return {Number} index -1 when not found
  4082. */
  4083. 'indexOf': function (object) {
  4084. var listeners = this.listeners;
  4085. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  4086. var listener = listeners[i];
  4087. if (listener && listener.object == object) {
  4088. return i;
  4089. }
  4090. }
  4091. return -1;
  4092. },
  4093. /**
  4094. * Add an event listener
  4095. * @param {Object} object
  4096. * @param {String} event The name of an event, for example 'select'
  4097. * @param {function} callback The callback method, called when the
  4098. * event takes place
  4099. */
  4100. 'addListener': function (object, event, callback) {
  4101. var index = this.indexOf(object);
  4102. var listener = this.listeners[index];
  4103. if (!listener) {
  4104. listener = {
  4105. 'object': object,
  4106. 'events': {}
  4107. };
  4108. this.listeners.push(listener);
  4109. }
  4110. var callbacks = listener.events[event];
  4111. if (!callbacks) {
  4112. callbacks = [];
  4113. listener.events[event] = callbacks;
  4114. }
  4115. // add the callback if it does not yet exist
  4116. if (callbacks.indexOf(callback) == -1) {
  4117. callbacks.push(callback);
  4118. }
  4119. },
  4120. /**
  4121. * Remove an event listener
  4122. * @param {Object} object
  4123. * @param {String} event The name of an event, for example 'select'
  4124. * @param {function} callback The registered callback method
  4125. */
  4126. 'removeListener': function (object, event, callback) {
  4127. var index = this.indexOf(object);
  4128. var listener = this.listeners[index];
  4129. if (listener) {
  4130. var callbacks = listener.events[event];
  4131. if (callbacks) {
  4132. index = callbacks.indexOf(callback);
  4133. if (index != -1) {
  4134. callbacks.splice(index, 1);
  4135. }
  4136. // remove the array when empty
  4137. if (callbacks.length == 0) {
  4138. delete listener.events[event];
  4139. }
  4140. }
  4141. // count the number of registered events. remove listener when empty
  4142. var count = 0;
  4143. var events = listener.events;
  4144. for (var e in events) {
  4145. if (events.hasOwnProperty(e)) {
  4146. count++;
  4147. }
  4148. }
  4149. if (count == 0) {
  4150. delete this.listeners[index];
  4151. }
  4152. }
  4153. },
  4154. /**
  4155. * Remove all registered event listeners
  4156. */
  4157. 'removeAllListeners': function () {
  4158. this.listeners = [];
  4159. },
  4160. /**
  4161. * Trigger an event. All registered event handlers will be called
  4162. * @param {Object} object
  4163. * @param {String} event
  4164. * @param {Object} properties (optional)
  4165. */
  4166. 'trigger': function (object, event, properties) {
  4167. var index = this.indexOf(object);
  4168. var listener = this.listeners[index];
  4169. if (listener) {
  4170. var callbacks = listener.events[event];
  4171. if (callbacks) {
  4172. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  4173. callbacks[i](properties);
  4174. }
  4175. }
  4176. }
  4177. }
  4178. };
  4179. /**
  4180. * An event bus can be used to emit events, and to subscribe to events
  4181. * @constructor EventBus
  4182. */
  4183. function EventBus() {
  4184. this.subscriptions = [];
  4185. }
  4186. /**
  4187. * Subscribe to an event
  4188. * @param {String | RegExp} event The event can be a regular expression, or
  4189. * a string with wildcards, like 'server.*'.
  4190. * @param {function} callback. Callback are called with three parameters:
  4191. * {String} event, {*} [data], {*} [source]
  4192. * @param {*} [target]
  4193. * @returns {String} id A subscription id
  4194. */
  4195. EventBus.prototype.on = function (event, callback, target) {
  4196. var regexp = (event instanceof RegExp) ?
  4197. event :
  4198. new RegExp(event.replace('*', '\\w+'));
  4199. var subscription = {
  4200. id: util.randomUUID(),
  4201. event: event,
  4202. regexp: regexp,
  4203. callback: (typeof callback === 'function') ? callback : null,
  4204. target: target
  4205. };
  4206. this.subscriptions.push(subscription);
  4207. return subscription.id;
  4208. };
  4209. /**
  4210. * Unsubscribe from an event
  4211. * @param {String | Object} filter Filter for subscriptions to be removed
  4212. * Filter can be a string containing a
  4213. * subscription id, or an object containing
  4214. * one or more of the fields id, event,
  4215. * callback, and target.
  4216. */
  4217. EventBus.prototype.off = function (filter) {
  4218. var i = 0;
  4219. while (i < this.subscriptions.length) {
  4220. var subscription = this.subscriptions[i];
  4221. var match = true;
  4222. if (filter instanceof Object) {
  4223. // filter is an object. All fields must match
  4224. for (var prop in filter) {
  4225. if (filter.hasOwnProperty(prop)) {
  4226. if (filter[prop] !== subscription[prop]) {
  4227. match = false;
  4228. }
  4229. }
  4230. }
  4231. }
  4232. else {
  4233. // filter is a string, filter on id
  4234. match = (subscription.id == filter);
  4235. }
  4236. if (match) {
  4237. this.subscriptions.splice(i, 1);
  4238. }
  4239. else {
  4240. i++;
  4241. }
  4242. }
  4243. };
  4244. /**
  4245. * Emit an event
  4246. * @param {String} event
  4247. * @param {*} [data]
  4248. * @param {*} [source]
  4249. */
  4250. EventBus.prototype.emit = function (event, data, source) {
  4251. for (var i =0; i < this.subscriptions.length; i++) {
  4252. var subscription = this.subscriptions[i];
  4253. if (subscription.regexp.test(event)) {
  4254. if (subscription.callback) {
  4255. subscription.callback(event, data, source);
  4256. }
  4257. }
  4258. }
  4259. };
  4260. /**
  4261. * DataSet
  4262. *
  4263. * Usage:
  4264. * var dataSet = new DataSet({
  4265. * fieldId: '_id',
  4266. * convert: {
  4267. * // ...
  4268. * }
  4269. * });
  4270. *
  4271. * dataSet.add(item);
  4272. * dataSet.add(data);
  4273. * dataSet.update(item);
  4274. * dataSet.update(data);
  4275. * dataSet.remove(id);
  4276. * dataSet.remove(ids);
  4277. * var data = dataSet.get();
  4278. * var data = dataSet.get(id);
  4279. * var data = dataSet.get(ids);
  4280. * var data = dataSet.get(ids, options, data);
  4281. * dataSet.clear();
  4282. *
  4283. * A data set can:
  4284. * - add/remove/update data
  4285. * - gives triggers upon changes in the data
  4286. * - can import/export data in various data formats
  4287. *
  4288. * @param {Object} [options] Available options:
  4289. * {String} fieldId Field name of the id in the
  4290. * items, 'id' by default.
  4291. * {Object.<String, String} convert
  4292. * A map with field names as key,
  4293. * and the field type as value.
  4294. * @constructor DataSet
  4295. */
  4296. // TODO: add a DataSet constructor DataSet(data, options)
  4297. function DataSet (options) {
  4298. this.id = util.randomUUID();
  4299. this.options = options || {};
  4300. this.data = {}; // map with data indexed by id
  4301. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  4302. this.convert = {}; // field types by field name
  4303. if (this.options.convert) {
  4304. for (var field in this.options.convert) {
  4305. if (this.options.convert.hasOwnProperty(field)) {
  4306. var value = this.options.convert[field];
  4307. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  4308. this.convert[field] = 'Date';
  4309. }
  4310. else {
  4311. this.convert[field] = value;
  4312. }
  4313. }
  4314. }
  4315. }
  4316. // event subscribers
  4317. this.subscribers = {};
  4318. this.internalIds = {}; // internally generated id's
  4319. }
  4320. /**
  4321. * Subscribe to an event, add an event listener
  4322. * @param {String} event Event name. Available events: 'put', 'update',
  4323. * 'remove'
  4324. * @param {function} callback Callback method. Called with three parameters:
  4325. * {String} event
  4326. * {Object | null} params
  4327. * {String | Number} senderId
  4328. */
  4329. DataSet.prototype.subscribe = function (event, callback) {
  4330. var subscribers = this.subscribers[event];
  4331. if (!subscribers) {
  4332. subscribers = [];
  4333. this.subscribers[event] = subscribers;
  4334. }
  4335. subscribers.push({
  4336. callback: callback
  4337. });
  4338. };
  4339. /**
  4340. * Unsubscribe from an event, remove an event listener
  4341. * @param {String} event
  4342. * @param {function} callback
  4343. */
  4344. DataSet.prototype.unsubscribe = function (event, callback) {
  4345. var subscribers = this.subscribers[event];
  4346. if (subscribers) {
  4347. this.subscribers[event] = subscribers.filter(function (listener) {
  4348. return (listener.callback != callback);
  4349. });
  4350. }
  4351. };
  4352. /**
  4353. * Trigger an event
  4354. * @param {String} event
  4355. * @param {Object | null} params
  4356. * @param {String} [senderId] Optional id of the sender.
  4357. * @private
  4358. */
  4359. DataSet.prototype._trigger = function (event, params, senderId) {
  4360. if (event == '*') {
  4361. throw new Error('Cannot trigger event *');
  4362. }
  4363. var subscribers = [];
  4364. if (event in this.subscribers) {
  4365. subscribers = subscribers.concat(this.subscribers[event]);
  4366. }
  4367. if ('*' in this.subscribers) {
  4368. subscribers = subscribers.concat(this.subscribers['*']);
  4369. }
  4370. for (var i = 0; i < subscribers.length; i++) {
  4371. var subscriber = subscribers[i];
  4372. if (subscriber.callback) {
  4373. subscriber.callback(event, params, senderId || null);
  4374. }
  4375. }
  4376. };
  4377. /**
  4378. * Add data.
  4379. * Adding an item will fail when there already is an item with the same id.
  4380. * @param {Object | Array | DataTable} data
  4381. * @param {String} [senderId] Optional sender id
  4382. * @return {Array} addedIds Array with the ids of the added items
  4383. */
  4384. DataSet.prototype.add = function (data, senderId) {
  4385. var addedIds = [],
  4386. id,
  4387. me = this;
  4388. if (data instanceof Array) {
  4389. // Array
  4390. for (var i = 0, len = data.length; i < len; i++) {
  4391. id = me._addItem(data[i]);
  4392. addedIds.push(id);
  4393. }
  4394. }
  4395. else if (util.isDataTable(data)) {
  4396. // Google DataTable
  4397. var columns = this._getColumnNames(data);
  4398. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  4399. var item = {};
  4400. for (var col = 0, cols = columns.length; col < cols; col++) {
  4401. var field = columns[col];
  4402. item[field] = data.getValue(row, col);
  4403. }
  4404. id = me._addItem(item);
  4405. addedIds.push(id);
  4406. }
  4407. }
  4408. else if (data instanceof Object) {
  4409. // Single item
  4410. id = me._addItem(data);
  4411. addedIds.push(id);
  4412. }
  4413. else {
  4414. throw new Error('Unknown dataType');
  4415. }
  4416. if (addedIds.length) {
  4417. this._trigger('add', {items: addedIds}, senderId);
  4418. }
  4419. return addedIds;
  4420. };
  4421. /**
  4422. * Update existing items. When an item does not exist, it will be created
  4423. * @param {Object | Array | DataTable} data
  4424. * @param {String} [senderId] Optional sender id
  4425. * @return {Array} updatedIds The ids of the added or updated items
  4426. */
  4427. DataSet.prototype.update = function (data, senderId) {
  4428. var addedIds = [],
  4429. updatedIds = [],
  4430. me = this,
  4431. fieldId = me.fieldId;
  4432. var addOrUpdate = function (item) {
  4433. var id = item[fieldId];
  4434. if (me.data[id]) {
  4435. // update item
  4436. id = me._updateItem(item);
  4437. updatedIds.push(id);
  4438. }
  4439. else {
  4440. // add new item
  4441. id = me._addItem(item);
  4442. addedIds.push(id);
  4443. }
  4444. };
  4445. if (data instanceof Array) {
  4446. // Array
  4447. for (var i = 0, len = data.length; i < len; i++) {
  4448. addOrUpdate(data[i]);
  4449. }
  4450. }
  4451. else if (util.isDataTable(data)) {
  4452. // Google DataTable
  4453. var columns = this._getColumnNames(data);
  4454. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  4455. var item = {};
  4456. for (var col = 0, cols = columns.length; col < cols; col++) {
  4457. var field = columns[col];
  4458. item[field] = data.getValue(row, col);
  4459. }
  4460. addOrUpdate(item);
  4461. }
  4462. }
  4463. else if (data instanceof Object) {
  4464. // Single item
  4465. addOrUpdate(data);
  4466. }
  4467. else {
  4468. throw new Error('Unknown dataType');
  4469. }
  4470. if (addedIds.length) {
  4471. this._trigger('add', {items: addedIds}, senderId);
  4472. }
  4473. if (updatedIds.length) {
  4474. this._trigger('update', {items: updatedIds}, senderId);
  4475. }
  4476. return addedIds.concat(updatedIds);
  4477. };
  4478. /**
  4479. * Get a data item or multiple items.
  4480. *
  4481. * Usage:
  4482. *
  4483. * get()
  4484. * get(options: Object)
  4485. * get(options: Object, data: Array | DataTable)
  4486. *
  4487. * get(id: Number | String)
  4488. * get(id: Number | String, options: Object)
  4489. * get(id: Number | String, options: Object, data: Array | DataTable)
  4490. *
  4491. * get(ids: Number[] | String[])
  4492. * get(ids: Number[] | String[], options: Object)
  4493. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  4494. *
  4495. * Where:
  4496. *
  4497. * {Number | String} id The id of an item
  4498. * {Number[] | String{}} ids An array with ids of items
  4499. * {Object} options An Object with options. Available options:
  4500. * {String} [type] Type of data to be returned. Can
  4501. * be 'DataTable' or 'Array' (default)
  4502. * {Object.<String, String>} [convert]
  4503. * {String[]} [fields] field names to be returned
  4504. * {function} [filter] filter items
  4505. * {String | function} [order] Order the items by
  4506. * a field name or custom sort function.
  4507. * {Array | DataTable} [data] If provided, items will be appended to this
  4508. * array or table. Required in case of Google
  4509. * DataTable.
  4510. *
  4511. * @throws Error
  4512. */
  4513. DataSet.prototype.get = function (args) {
  4514. var me = this;
  4515. // parse the arguments
  4516. var id, ids, options, data;
  4517. var firstType = util.getType(arguments[0]);
  4518. if (firstType == 'String' || firstType == 'Number') {
  4519. // get(id [, options] [, data])
  4520. id = arguments[0];
  4521. options = arguments[1];
  4522. data = arguments[2];
  4523. }
  4524. else if (firstType == 'Array') {
  4525. // get(ids [, options] [, data])
  4526. ids = arguments[0];
  4527. options = arguments[1];
  4528. data = arguments[2];
  4529. }
  4530. else {
  4531. // get([, options] [, data])
  4532. options = arguments[0];
  4533. data = arguments[1];
  4534. }
  4535. // determine the return type
  4536. var type;
  4537. if (options && options.type) {
  4538. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  4539. if (data && (type != util.getType(data))) {
  4540. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  4541. 'does not correspond with specified options.type (' + options.type + ')');
  4542. }
  4543. if (type == 'DataTable' && !util.isDataTable(data)) {
  4544. throw new Error('Parameter "data" must be a DataTable ' +
  4545. 'when options.type is "DataTable"');
  4546. }
  4547. }
  4548. else if (data) {
  4549. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  4550. }
  4551. else {
  4552. type = 'Array';
  4553. }
  4554. // build options
  4555. var convert = options && options.convert || this.options.convert;
  4556. var filter = options && options.filter;
  4557. var items = [], item, itemId, i, len;
  4558. // convert items
  4559. if (id != undefined) {
  4560. // return a single item
  4561. item = me._getItem(id, convert);
  4562. if (filter && !filter(item)) {
  4563. item = null;
  4564. }
  4565. }
  4566. else if (ids != undefined) {
  4567. // return a subset of items
  4568. for (i = 0, len = ids.length; i < len; i++) {
  4569. item = me._getItem(ids[i], convert);
  4570. if (!filter || filter(item)) {
  4571. items.push(item);
  4572. }
  4573. }
  4574. }
  4575. else {
  4576. // return all items
  4577. for (itemId in this.data) {
  4578. if (this.data.hasOwnProperty(itemId)) {
  4579. item = me._getItem(itemId, convert);
  4580. if (!filter || filter(item)) {
  4581. items.push(item);
  4582. }
  4583. }
  4584. }
  4585. }
  4586. // order the results
  4587. if (options && options.order && id == undefined) {
  4588. this._sort(items, options.order);
  4589. }
  4590. // filter fields of the items
  4591. if (options && options.fields) {
  4592. var fields = options.fields;
  4593. if (id != undefined) {
  4594. item = this._filterFields(item, fields);
  4595. }
  4596. else {
  4597. for (i = 0, len = items.length; i < len; i++) {
  4598. items[i] = this._filterFields(items[i], fields);
  4599. }
  4600. }
  4601. }
  4602. // return the results
  4603. if (type == 'DataTable') {
  4604. var columns = this._getColumnNames(data);
  4605. if (id != undefined) {
  4606. // append a single item to the data table
  4607. me._appendRow(data, columns, item);
  4608. }
  4609. else {
  4610. // copy the items to the provided data table
  4611. for (i = 0, len = items.length; i < len; i++) {
  4612. me._appendRow(data, columns, items[i]);
  4613. }
  4614. }
  4615. return data;
  4616. }
  4617. else {
  4618. // return an array
  4619. if (id != undefined) {
  4620. // a single item
  4621. return item;
  4622. }
  4623. else {
  4624. // multiple items
  4625. if (data) {
  4626. // copy the items to the provided array
  4627. for (i = 0, len = items.length; i < len; i++) {
  4628. data.push(items[i]);
  4629. }
  4630. return data;
  4631. }
  4632. else {
  4633. // just return our array
  4634. return items;
  4635. }
  4636. }
  4637. }
  4638. };
  4639. /**
  4640. * Get ids of all items or from a filtered set of items.
  4641. * @param {Object} [options] An Object with options. Available options:
  4642. * {function} [filter] filter items
  4643. * {String | function} [order] Order the items by
  4644. * a field name or custom sort function.
  4645. * @return {Array} ids
  4646. */
  4647. DataSet.prototype.getIds = function (options) {
  4648. var data = this.data,
  4649. filter = options && options.filter,
  4650. order = options && options.order,
  4651. convert = options && options.convert || this.options.convert,
  4652. i,
  4653. len,
  4654. id,
  4655. item,
  4656. items,
  4657. ids = [];
  4658. if (filter) {
  4659. // get filtered items
  4660. if (order) {
  4661. // create ordered list
  4662. items = [];
  4663. for (id in data) {
  4664. if (data.hasOwnProperty(id)) {
  4665. item = this._getItem(id, convert);
  4666. if (filter(item)) {
  4667. items.push(item);
  4668. }
  4669. }
  4670. }
  4671. this._sort(items, order);
  4672. for (i = 0, len = items.length; i < len; i++) {
  4673. ids[i] = items[i][this.fieldId];
  4674. }
  4675. }
  4676. else {
  4677. // create unordered list
  4678. for (id in data) {
  4679. if (data.hasOwnProperty(id)) {
  4680. item = this._getItem(id, convert);
  4681. if (filter(item)) {
  4682. ids.push(item[this.fieldId]);
  4683. }
  4684. }
  4685. }
  4686. }
  4687. }
  4688. else {
  4689. // get all items
  4690. if (order) {
  4691. // create an ordered list
  4692. items = [];
  4693. for (id in data) {
  4694. if (data.hasOwnProperty(id)) {
  4695. items.push(data[id]);
  4696. }
  4697. }
  4698. this._sort(items, order);
  4699. for (i = 0, len = items.length; i < len; i++) {
  4700. ids[i] = items[i][this.fieldId];
  4701. }
  4702. }
  4703. else {
  4704. // create unordered list
  4705. for (id in data) {
  4706. if (data.hasOwnProperty(id)) {
  4707. item = data[id];
  4708. ids.push(item[this.fieldId]);
  4709. }
  4710. }
  4711. }
  4712. }
  4713. return ids;
  4714. };
  4715. /**
  4716. * Execute a callback function for every item in the dataset.
  4717. * The order of the items is not determined.
  4718. * @param {function} callback
  4719. * @param {Object} [options] Available options:
  4720. * {Object.<String, String>} [convert]
  4721. * {String[]} [fields] filter fields
  4722. * {function} [filter] filter items
  4723. * {String | function} [order] Order the items by
  4724. * a field name or custom sort function.
  4725. */
  4726. DataSet.prototype.forEach = function (callback, options) {
  4727. var filter = options && options.filter,
  4728. convert = options && options.convert || this.options.convert,
  4729. data = this.data,
  4730. item,
  4731. id;
  4732. if (options && options.order) {
  4733. // execute forEach on ordered list
  4734. var items = this.get(options);
  4735. for (var i = 0, len = items.length; i < len; i++) {
  4736. item = items[i];
  4737. id = item[this.fieldId];
  4738. callback(item, id);
  4739. }
  4740. }
  4741. else {
  4742. // unordered
  4743. for (id in data) {
  4744. if (data.hasOwnProperty(id)) {
  4745. item = this._getItem(id, convert);
  4746. if (!filter || filter(item)) {
  4747. callback(item, id);
  4748. }
  4749. }
  4750. }
  4751. }
  4752. };
  4753. /**
  4754. * Map every item in the dataset.
  4755. * @param {function} callback
  4756. * @param {Object} [options] Available options:
  4757. * {Object.<String, String>} [convert]
  4758. * {String[]} [fields] filter fields
  4759. * {function} [filter] filter items
  4760. * {String | function} [order] Order the items by
  4761. * a field name or custom sort function.
  4762. * @return {Object[]} mappedItems
  4763. */
  4764. DataSet.prototype.map = function (callback, options) {
  4765. var filter = options && options.filter,
  4766. convert = options && options.convert || this.options.convert,
  4767. mappedItems = [],
  4768. data = this.data,
  4769. item;
  4770. // convert and filter items
  4771. for (var id in data) {
  4772. if (data.hasOwnProperty(id)) {
  4773. item = this._getItem(id, convert);
  4774. if (!filter || filter(item)) {
  4775. mappedItems.push(callback(item, id));
  4776. }
  4777. }
  4778. }
  4779. // order items
  4780. if (options && options.order) {
  4781. this._sort(mappedItems, options.order);
  4782. }
  4783. return mappedItems;
  4784. };
  4785. /**
  4786. * Filter the fields of an item
  4787. * @param {Object} item
  4788. * @param {String[]} fields Field names
  4789. * @return {Object} filteredItem
  4790. * @private
  4791. */
  4792. DataSet.prototype._filterFields = function (item, fields) {
  4793. var filteredItem = {};
  4794. for (var field in item) {
  4795. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  4796. filteredItem[field] = item[field];
  4797. }
  4798. }
  4799. return filteredItem;
  4800. };
  4801. /**
  4802. * Sort the provided array with items
  4803. * @param {Object[]} items
  4804. * @param {String | function} order A field name or custom sort function.
  4805. * @private
  4806. */
  4807. DataSet.prototype._sort = function (items, order) {
  4808. if (util.isString(order)) {
  4809. // order by provided field name
  4810. var name = order; // field name
  4811. items.sort(function (a, b) {
  4812. var av = a[name];
  4813. var bv = b[name];
  4814. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  4815. });
  4816. }
  4817. else if (typeof order === 'function') {
  4818. // order by sort function
  4819. items.sort(order);
  4820. }
  4821. // TODO: extend order by an Object {field:String, direction:String}
  4822. // where direction can be 'asc' or 'desc'
  4823. else {
  4824. throw new TypeError('Order must be a function or a string');
  4825. }
  4826. };
  4827. /**
  4828. * Remove an object by pointer or by id
  4829. * @param {String | Number | Object | Array} id Object or id, or an array with
  4830. * objects or ids to be removed
  4831. * @param {String} [senderId] Optional sender id
  4832. * @return {Array} removedIds
  4833. */
  4834. DataSet.prototype.remove = function (id, senderId) {
  4835. var removedIds = [],
  4836. i, len, removedId;
  4837. if (id instanceof Array) {
  4838. for (i = 0, len = id.length; i < len; i++) {
  4839. removedId = this._remove(id[i]);
  4840. if (removedId != null) {
  4841. removedIds.push(removedId);
  4842. }
  4843. }
  4844. }
  4845. else {
  4846. removedId = this._remove(id);
  4847. if (removedId != null) {
  4848. removedIds.push(removedId);
  4849. }
  4850. }
  4851. if (removedIds.length) {
  4852. this._trigger('remove', {items: removedIds}, senderId);
  4853. }
  4854. return removedIds;
  4855. };
  4856. /**
  4857. * Remove an item by its id
  4858. * @param {Number | String | Object} id id or item
  4859. * @returns {Number | String | null} id
  4860. * @private
  4861. */
  4862. DataSet.prototype._remove = function (id) {
  4863. if (util.isNumber(id) || util.isString(id)) {
  4864. if (this.data[id]) {
  4865. delete this.data[id];
  4866. delete this.internalIds[id];
  4867. return id;
  4868. }
  4869. }
  4870. else if (id instanceof Object) {
  4871. var itemId = id[this.fieldId];
  4872. if (itemId && this.data[itemId]) {
  4873. delete this.data[itemId];
  4874. delete this.internalIds[itemId];
  4875. return itemId;
  4876. }
  4877. }
  4878. return null;
  4879. };
  4880. /**
  4881. * Clear the data
  4882. * @param {String} [senderId] Optional sender id
  4883. * @return {Array} removedIds The ids of all removed items
  4884. */
  4885. DataSet.prototype.clear = function (senderId) {
  4886. var ids = Object.keys(this.data);
  4887. this.data = {};
  4888. this.internalIds = {};
  4889. this._trigger('remove', {items: ids}, senderId);
  4890. return ids;
  4891. };
  4892. /**
  4893. * Find the item with maximum value of a specified field
  4894. * @param {String} field
  4895. * @return {Object | null} item Item containing max value, or null if no items
  4896. */
  4897. DataSet.prototype.max = function (field) {
  4898. var data = this.data,
  4899. max = null,
  4900. maxField = null;
  4901. for (var id in data) {
  4902. if (data.hasOwnProperty(id)) {
  4903. var item = data[id];
  4904. var itemField = item[field];
  4905. if (itemField != null && (!max || itemField > maxField)) {
  4906. max = item;
  4907. maxField = itemField;
  4908. }
  4909. }
  4910. }
  4911. return max;
  4912. };
  4913. /**
  4914. * Find the item with minimum value of a specified field
  4915. * @param {String} field
  4916. * @return {Object | null} item Item containing max value, or null if no items
  4917. */
  4918. DataSet.prototype.min = function (field) {
  4919. var data = this.data,
  4920. min = null,
  4921. minField = null;
  4922. for (var id in data) {
  4923. if (data.hasOwnProperty(id)) {
  4924. var item = data[id];
  4925. var itemField = item[field];
  4926. if (itemField != null && (!min || itemField < minField)) {
  4927. min = item;
  4928. minField = itemField;
  4929. }
  4930. }
  4931. }
  4932. return min;
  4933. };
  4934. /**
  4935. * Find all distinct values of a specified field
  4936. * @param {String} field
  4937. * @return {Array} values Array containing all distinct values. If the data
  4938. * items do not contain the specified field, an array
  4939. * containing a single value undefined is returned.
  4940. * The returned array is unordered.
  4941. */
  4942. DataSet.prototype.distinct = function (field) {
  4943. var data = this.data,
  4944. values = [],
  4945. fieldType = this.options.convert[field],
  4946. count = 0;
  4947. for (var prop in data) {
  4948. if (data.hasOwnProperty(prop)) {
  4949. var item = data[prop];
  4950. var value = util.convert(item[field], fieldType);
  4951. var exists = false;
  4952. for (var i = 0; i < count; i++) {
  4953. if (values[i] == value) {
  4954. exists = true;
  4955. break;
  4956. }
  4957. }
  4958. if (!exists) {
  4959. values[count] = value;
  4960. count++;
  4961. }
  4962. }
  4963. }
  4964. return values;
  4965. };
  4966. /**
  4967. * Add a single item. Will fail when an item with the same id already exists.
  4968. * @param {Object} item
  4969. * @return {String} id
  4970. * @private
  4971. */
  4972. DataSet.prototype._addItem = function (item) {
  4973. var id = item[this.fieldId];
  4974. if (id != undefined) {
  4975. // check whether this id is already taken
  4976. if (this.data[id]) {
  4977. // item already exists
  4978. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  4979. }
  4980. }
  4981. else {
  4982. // generate an id
  4983. id = util.randomUUID();
  4984. item[this.fieldId] = id;
  4985. this.internalIds[id] = item;
  4986. }
  4987. var d = {};
  4988. for (var field in item) {
  4989. if (item.hasOwnProperty(field)) {
  4990. var fieldType = this.convert[field]; // type may be undefined
  4991. d[field] = util.convert(item[field], fieldType);
  4992. }
  4993. }
  4994. this.data[id] = d;
  4995. return id;
  4996. };
  4997. /**
  4998. * Get an item. Fields can be converted to a specific type
  4999. * @param {String} id
  5000. * @param {Object.<String, String>} [convert] field types to convert
  5001. * @return {Object | null} item
  5002. * @private
  5003. */
  5004. DataSet.prototype._getItem = function (id, convert) {
  5005. var field, value;
  5006. // get the item from the dataset
  5007. var raw = this.data[id];
  5008. if (!raw) {
  5009. return null;
  5010. }
  5011. // convert the items field types
  5012. var converted = {},
  5013. fieldId = this.fieldId,
  5014. internalIds = this.internalIds;
  5015. if (convert) {
  5016. for (field in raw) {
  5017. if (raw.hasOwnProperty(field)) {
  5018. value = raw[field];
  5019. // output all fields, except internal ids
  5020. if ((field != fieldId) || !(value in internalIds)) {
  5021. converted[field] = util.convert(value, convert[field]);
  5022. }
  5023. }
  5024. }
  5025. }
  5026. else {
  5027. // no field types specified, no converting needed
  5028. for (field in raw) {
  5029. if (raw.hasOwnProperty(field)) {
  5030. value = raw[field];
  5031. // output all fields, except internal ids
  5032. if ((field != fieldId) || !(value in internalIds)) {
  5033. converted[field] = value;
  5034. }
  5035. }
  5036. }
  5037. }
  5038. return converted;
  5039. };
  5040. /**
  5041. * Update a single item: merge with existing item.
  5042. * Will fail when the item has no id, or when there does not exist an item
  5043. * with the same id.
  5044. * @param {Object} item
  5045. * @return {String} id
  5046. * @private
  5047. */
  5048. DataSet.prototype._updateItem = function (item) {
  5049. var id = item[this.fieldId];
  5050. if (id == undefined) {
  5051. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  5052. }
  5053. var d = this.data[id];
  5054. if (!d) {
  5055. // item doesn't exist
  5056. throw new Error('Cannot update item: no item with id ' + id + ' found');
  5057. }
  5058. // merge with current item
  5059. for (var field in item) {
  5060. if (item.hasOwnProperty(field)) {
  5061. var fieldType = this.convert[field]; // type may be undefined
  5062. d[field] = util.convert(item[field], fieldType);
  5063. }
  5064. }
  5065. return id;
  5066. };
  5067. /**
  5068. * Get an array with the column names of a Google DataTable
  5069. * @param {DataTable} dataTable
  5070. * @return {String[]} columnNames
  5071. * @private
  5072. */
  5073. DataSet.prototype._getColumnNames = function (dataTable) {
  5074. var columns = [];
  5075. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  5076. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  5077. }
  5078. return columns;
  5079. };
  5080. /**
  5081. * Append an item as a row to the dataTable
  5082. * @param dataTable
  5083. * @param columns
  5084. * @param item
  5085. * @private
  5086. */
  5087. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  5088. var row = dataTable.addRow();
  5089. for (var col = 0, cols = columns.length; col < cols; col++) {
  5090. var field = columns[col];
  5091. dataTable.setValue(row, col, item[field]);
  5092. }
  5093. };
  5094. /**
  5095. * DataView
  5096. *
  5097. * a dataview offers a filtered view on a dataset or an other dataview.
  5098. *
  5099. * @param {DataSet | DataView} data
  5100. * @param {Object} [options] Available options: see method get
  5101. *
  5102. * @constructor DataView
  5103. */
  5104. function DataView (data, options) {
  5105. this.id = util.randomUUID();
  5106. this.data = null;
  5107. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  5108. this.options = options || {};
  5109. this.fieldId = 'id'; // name of the field containing id
  5110. this.subscribers = {}; // event subscribers
  5111. var me = this;
  5112. this.listener = function () {
  5113. me._onEvent.apply(me, arguments);
  5114. };
  5115. this.setData(data);
  5116. }
  5117. /**
  5118. * Set a data source for the view
  5119. * @param {DataSet | DataView} data
  5120. */
  5121. DataView.prototype.setData = function (data) {
  5122. var ids, dataItems, i, len;
  5123. if (this.data) {
  5124. // unsubscribe from current dataset
  5125. if (this.data.unsubscribe) {
  5126. this.data.unsubscribe('*', this.listener);
  5127. }
  5128. // trigger a remove of all items in memory
  5129. ids = [];
  5130. for (var id in this.ids) {
  5131. if (this.ids.hasOwnProperty(id)) {
  5132. ids.push(id);
  5133. }
  5134. }
  5135. this.ids = {};
  5136. this._trigger('remove', {items: ids});
  5137. }
  5138. this.data = data;
  5139. if (this.data) {
  5140. // update fieldId
  5141. this.fieldId = this.options.fieldId ||
  5142. (this.data && this.data.options && this.data.options.fieldId) ||
  5143. 'id';
  5144. // trigger an add of all added items
  5145. ids = this.data.getIds({filter: this.options && this.options.filter});
  5146. for (i = 0, len = ids.length; i < len; i++) {
  5147. id = ids[i];
  5148. this.ids[id] = true;
  5149. }
  5150. this._trigger('add', {items: ids});
  5151. // subscribe to new dataset
  5152. if (this.data.subscribe) {
  5153. this.data.subscribe('*', this.listener);
  5154. }
  5155. }
  5156. };
  5157. /**
  5158. * Get data from the data view
  5159. *
  5160. * Usage:
  5161. *
  5162. * get()
  5163. * get(options: Object)
  5164. * get(options: Object, data: Array | DataTable)
  5165. *
  5166. * get(id: Number)
  5167. * get(id: Number, options: Object)
  5168. * get(id: Number, options: Object, data: Array | DataTable)
  5169. *
  5170. * get(ids: Number[])
  5171. * get(ids: Number[], options: Object)
  5172. * get(ids: Number[], options: Object, data: Array | DataTable)
  5173. *
  5174. * Where:
  5175. *
  5176. * {Number | String} id The id of an item
  5177. * {Number[] | String{}} ids An array with ids of items
  5178. * {Object} options An Object with options. Available options:
  5179. * {String} [type] Type of data to be returned. Can
  5180. * be 'DataTable' or 'Array' (default)
  5181. * {Object.<String, String>} [convert]
  5182. * {String[]} [fields] field names to be returned
  5183. * {function} [filter] filter items
  5184. * {String | function} [order] Order the items by
  5185. * a field name or custom sort function.
  5186. * {Array | DataTable} [data] If provided, items will be appended to this
  5187. * array or table. Required in case of Google
  5188. * DataTable.
  5189. * @param args
  5190. */
  5191. DataView.prototype.get = function (args) {
  5192. var me = this;
  5193. // parse the arguments
  5194. var ids, options, data;
  5195. var firstType = util.getType(arguments[0]);
  5196. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  5197. // get(id(s) [, options] [, data])
  5198. ids = arguments[0]; // can be a single id or an array with ids
  5199. options = arguments[1];
  5200. data = arguments[2];
  5201. }
  5202. else {
  5203. // get([, options] [, data])
  5204. options = arguments[0];
  5205. data = arguments[1];
  5206. }
  5207. // extend the options with the default options and provided options
  5208. var viewOptions = util.extend({}, this.options, options);
  5209. // create a combined filter method when needed
  5210. if (this.options.filter && options && options.filter) {
  5211. viewOptions.filter = function (item) {
  5212. return me.options.filter(item) && options.filter(item);
  5213. }
  5214. }
  5215. // build up the call to the linked data set
  5216. var getArguments = [];
  5217. if (ids != undefined) {
  5218. getArguments.push(ids);
  5219. }
  5220. getArguments.push(viewOptions);
  5221. getArguments.push(data);
  5222. return this.data && this.data.get.apply(this.data, getArguments);
  5223. };
  5224. /**
  5225. * Get ids of all items or from a filtered set of items.
  5226. * @param {Object} [options] An Object with options. Available options:
  5227. * {function} [filter] filter items
  5228. * {String | function} [order] Order the items by
  5229. * a field name or custom sort function.
  5230. * @return {Array} ids
  5231. */
  5232. DataView.prototype.getIds = function (options) {
  5233. var ids;
  5234. if (this.data) {
  5235. var defaultFilter = this.options.filter;
  5236. var filter;
  5237. if (options && options.filter) {
  5238. if (defaultFilter) {
  5239. filter = function (item) {
  5240. return defaultFilter(item) && options.filter(item);
  5241. }
  5242. }
  5243. else {
  5244. filter = options.filter;
  5245. }
  5246. }
  5247. else {
  5248. filter = defaultFilter;
  5249. }
  5250. ids = this.data.getIds({
  5251. filter: filter,
  5252. order: options && options.order
  5253. });
  5254. }
  5255. else {
  5256. ids = [];
  5257. }
  5258. return ids;
  5259. };
  5260. /**
  5261. * Event listener. Will propagate all events from the connected data set to
  5262. * the subscribers of the DataView, but will filter the items and only trigger
  5263. * when there are changes in the filtered data set.
  5264. * @param {String} event
  5265. * @param {Object | null} params
  5266. * @param {String} senderId
  5267. * @private
  5268. */
  5269. DataView.prototype._onEvent = function (event, params, senderId) {
  5270. var i, len, id, item,
  5271. ids = params && params.items,
  5272. data = this.data,
  5273. added = [],
  5274. updated = [],
  5275. removed = [];
  5276. if (ids && data) {
  5277. switch (event) {
  5278. case 'add':
  5279. // filter the ids of the added items
  5280. for (i = 0, len = ids.length; i < len; i++) {
  5281. id = ids[i];
  5282. item = this.get(id);
  5283. if (item) {
  5284. this.ids[id] = true;
  5285. added.push(id);
  5286. }
  5287. }
  5288. break;
  5289. case 'update':
  5290. // determine the event from the views viewpoint: an updated
  5291. // item can be added, updated, or removed from this view.
  5292. for (i = 0, len = ids.length; i < len; i++) {
  5293. id = ids[i];
  5294. item = this.get(id);
  5295. if (item) {
  5296. if (this.ids[id]) {
  5297. updated.push(id);
  5298. }
  5299. else {
  5300. this.ids[id] = true;
  5301. added.push(id);
  5302. }
  5303. }
  5304. else {
  5305. if (this.ids[id]) {
  5306. delete this.ids[id];
  5307. removed.push(id);
  5308. }
  5309. else {
  5310. // nothing interesting for me :-(
  5311. }
  5312. }
  5313. }
  5314. break;
  5315. case 'remove':
  5316. // filter the ids of the removed items
  5317. for (i = 0, len = ids.length; i < len; i++) {
  5318. id = ids[i];
  5319. if (this.ids[id]) {
  5320. delete this.ids[id];
  5321. removed.push(id);
  5322. }
  5323. }
  5324. break;
  5325. }
  5326. if (added.length) {
  5327. this._trigger('add', {items: added}, senderId);
  5328. }
  5329. if (updated.length) {
  5330. this._trigger('update', {items: updated}, senderId);
  5331. }
  5332. if (removed.length) {
  5333. this._trigger('remove', {items: removed}, senderId);
  5334. }
  5335. }
  5336. };
  5337. // copy subscription functionality from DataSet
  5338. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  5339. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  5340. DataView.prototype._trigger = DataSet.prototype._trigger;
  5341. /**
  5342. * @constructor TimeStep
  5343. * The class TimeStep is an iterator for dates. You provide a start date and an
  5344. * end date. The class itself determines the best scale (step size) based on the
  5345. * provided start Date, end Date, and minimumStep.
  5346. *
  5347. * If minimumStep is provided, the step size is chosen as close as possible
  5348. * to the minimumStep but larger than minimumStep. If minimumStep is not
  5349. * provided, the scale is set to 1 DAY.
  5350. * The minimumStep should correspond with the onscreen size of about 6 characters
  5351. *
  5352. * Alternatively, you can set a scale by hand.
  5353. * After creation, you can initialize the class by executing first(). Then you
  5354. * can iterate from the start date to the end date via next(). You can check if
  5355. * the end date is reached with the function hasNext(). After each step, you can
  5356. * retrieve the current date via getCurrent().
  5357. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  5358. * days, to years.
  5359. *
  5360. * Version: 1.2
  5361. *
  5362. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  5363. * or new Date(2010, 9, 21, 23, 45, 00)
  5364. * @param {Date} [end] The end date
  5365. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  5366. */
  5367. TimeStep = function(start, end, minimumStep) {
  5368. // variables
  5369. this.current = new Date();
  5370. this._start = new Date();
  5371. this._end = new Date();
  5372. this.autoScale = true;
  5373. this.scale = TimeStep.SCALE.DAY;
  5374. this.step = 1;
  5375. // initialize the range
  5376. this.setRange(start, end, minimumStep);
  5377. };
  5378. /// enum scale
  5379. TimeStep.SCALE = {
  5380. MILLISECOND: 1,
  5381. SECOND: 2,
  5382. MINUTE: 3,
  5383. HOUR: 4,
  5384. DAY: 5,
  5385. WEEKDAY: 6,
  5386. MONTH: 7,
  5387. YEAR: 8
  5388. };
  5389. /**
  5390. * Set a new range
  5391. * If minimumStep is provided, the step size is chosen as close as possible
  5392. * to the minimumStep but larger than minimumStep. If minimumStep is not
  5393. * provided, the scale is set to 1 DAY.
  5394. * The minimumStep should correspond with the onscreen size of about 6 characters
  5395. * @param {Date} [start] The start date and time.
  5396. * @param {Date} [end] The end date and time.
  5397. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  5398. */
  5399. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  5400. if (!(start instanceof Date) || !(end instanceof Date)) {
  5401. throw "No legal start or end date in method setRange";
  5402. }
  5403. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  5404. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  5405. if (this.autoScale) {
  5406. this.setMinimumStep(minimumStep);
  5407. }
  5408. };
  5409. /**
  5410. * Set the range iterator to the start date.
  5411. */
  5412. TimeStep.prototype.first = function() {
  5413. this.current = new Date(this._start.valueOf());
  5414. this.roundToMinor();
  5415. };
  5416. /**
  5417. * Round the current date to the first minor date value
  5418. * This must be executed once when the current date is set to start Date
  5419. */
  5420. TimeStep.prototype.roundToMinor = function() {
  5421. // round to floor
  5422. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  5423. //noinspection FallthroughInSwitchStatementJS
  5424. switch (this.scale) {
  5425. case TimeStep.SCALE.YEAR:
  5426. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  5427. this.current.setMonth(0);
  5428. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  5429. case TimeStep.SCALE.DAY: // intentional fall through
  5430. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  5431. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  5432. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  5433. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  5434. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  5435. }
  5436. if (this.step != 1) {
  5437. // round down to the first minor value that is a multiple of the current step size
  5438. switch (this.scale) {
  5439. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  5440. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  5441. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  5442. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  5443. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5444. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  5445. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  5446. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  5447. default: break;
  5448. }
  5449. }
  5450. };
  5451. /**
  5452. * Check if the there is a next step
  5453. * @return {boolean} true if the current date has not passed the end date
  5454. */
  5455. TimeStep.prototype.hasNext = function () {
  5456. return (this.current.valueOf() <= this._end.valueOf());
  5457. };
  5458. /**
  5459. * Do the next step
  5460. */
  5461. TimeStep.prototype.next = function() {
  5462. var prev = this.current.valueOf();
  5463. // Two cases, needed to prevent issues with switching daylight savings
  5464. // (end of March and end of October)
  5465. if (this.current.getMonth() < 6) {
  5466. switch (this.scale) {
  5467. case TimeStep.SCALE.MILLISECOND:
  5468. this.current = new Date(this.current.valueOf() + this.step); break;
  5469. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  5470. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  5471. case TimeStep.SCALE.HOUR:
  5472. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  5473. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  5474. var h = this.current.getHours();
  5475. this.current.setHours(h - (h % this.step));
  5476. break;
  5477. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5478. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  5479. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  5480. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  5481. default: break;
  5482. }
  5483. }
  5484. else {
  5485. switch (this.scale) {
  5486. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  5487. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  5488. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  5489. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  5490. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5491. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  5492. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  5493. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  5494. default: break;
  5495. }
  5496. }
  5497. if (this.step != 1) {
  5498. // round down to the correct major value
  5499. switch (this.scale) {
  5500. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  5501. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  5502. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  5503. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  5504. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5505. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  5506. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  5507. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  5508. default: break;
  5509. }
  5510. }
  5511. // safety mechanism: if current time is still unchanged, move to the end
  5512. if (this.current.valueOf() == prev) {
  5513. this.current = new Date(this._end.valueOf());
  5514. }
  5515. };
  5516. /**
  5517. * Get the current datetime
  5518. * @return {Date} current The current date
  5519. */
  5520. TimeStep.prototype.getCurrent = function() {
  5521. return this.current;
  5522. };
  5523. /**
  5524. * Set a custom scale. Autoscaling will be disabled.
  5525. * For example setScale(SCALE.MINUTES, 5) will result
  5526. * in minor steps of 5 minutes, and major steps of an hour.
  5527. *
  5528. * @param {TimeStep.SCALE} newScale
  5529. * A scale. Choose from SCALE.MILLISECOND,
  5530. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  5531. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  5532. * SCALE.YEAR.
  5533. * @param {Number} newStep A step size, by default 1. Choose for
  5534. * example 1, 2, 5, or 10.
  5535. */
  5536. TimeStep.prototype.setScale = function(newScale, newStep) {
  5537. this.scale = newScale;
  5538. if (newStep > 0) {
  5539. this.step = newStep;
  5540. }
  5541. this.autoScale = false;
  5542. };
  5543. /**
  5544. * Enable or disable autoscaling
  5545. * @param {boolean} enable If true, autoascaling is set true
  5546. */
  5547. TimeStep.prototype.setAutoScale = function (enable) {
  5548. this.autoScale = enable;
  5549. };
  5550. /**
  5551. * Automatically determine the scale that bests fits the provided minimum step
  5552. * @param {Number} [minimumStep] The minimum step size in milliseconds
  5553. */
  5554. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  5555. if (minimumStep == undefined) {
  5556. return;
  5557. }
  5558. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  5559. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  5560. var stepDay = (1000 * 60 * 60 * 24);
  5561. var stepHour = (1000 * 60 * 60);
  5562. var stepMinute = (1000 * 60);
  5563. var stepSecond = (1000);
  5564. var stepMillisecond= (1);
  5565. // find the smallest step that is larger than the provided minimumStep
  5566. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  5567. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  5568. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  5569. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  5570. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  5571. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  5572. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  5573. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  5574. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  5575. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  5576. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  5577. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  5578. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  5579. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  5580. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  5581. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  5582. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  5583. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  5584. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  5585. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  5586. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  5587. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  5588. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  5589. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  5590. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  5591. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  5592. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  5593. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  5594. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  5595. };
  5596. /**
  5597. * Snap a date to a rounded value. The snap intervals are dependent on the
  5598. * current scale and step.
  5599. * @param {Date} date the date to be snapped
  5600. */
  5601. TimeStep.prototype.snap = function(date) {
  5602. if (this.scale == TimeStep.SCALE.YEAR) {
  5603. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  5604. date.setFullYear(Math.round(year / this.step) * this.step);
  5605. date.setMonth(0);
  5606. date.setDate(0);
  5607. date.setHours(0);
  5608. date.setMinutes(0);
  5609. date.setSeconds(0);
  5610. date.setMilliseconds(0);
  5611. }
  5612. else if (this.scale == TimeStep.SCALE.MONTH) {
  5613. if (date.getDate() > 15) {
  5614. date.setDate(1);
  5615. date.setMonth(date.getMonth() + 1);
  5616. // important: first set Date to 1, after that change the month.
  5617. }
  5618. else {
  5619. date.setDate(1);
  5620. }
  5621. date.setHours(0);
  5622. date.setMinutes(0);
  5623. date.setSeconds(0);
  5624. date.setMilliseconds(0);
  5625. }
  5626. else if (this.scale == TimeStep.SCALE.DAY ||
  5627. this.scale == TimeStep.SCALE.WEEKDAY) {
  5628. //noinspection FallthroughInSwitchStatementJS
  5629. switch (this.step) {
  5630. case 5:
  5631. case 2:
  5632. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  5633. default:
  5634. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  5635. }
  5636. date.setMinutes(0);
  5637. date.setSeconds(0);
  5638. date.setMilliseconds(0);
  5639. }
  5640. else if (this.scale == TimeStep.SCALE.HOUR) {
  5641. switch (this.step) {
  5642. case 4:
  5643. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  5644. default:
  5645. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  5646. }
  5647. date.setSeconds(0);
  5648. date.setMilliseconds(0);
  5649. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  5650. //noinspection FallthroughInSwitchStatementJS
  5651. switch (this.step) {
  5652. case 15:
  5653. case 10:
  5654. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  5655. date.setSeconds(0);
  5656. break;
  5657. case 5:
  5658. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  5659. default:
  5660. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  5661. }
  5662. date.setMilliseconds(0);
  5663. }
  5664. else if (this.scale == TimeStep.SCALE.SECOND) {
  5665. //noinspection FallthroughInSwitchStatementJS
  5666. switch (this.step) {
  5667. case 15:
  5668. case 10:
  5669. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  5670. date.setMilliseconds(0);
  5671. break;
  5672. case 5:
  5673. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  5674. default:
  5675. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  5676. }
  5677. }
  5678. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  5679. var step = this.step > 5 ? this.step / 2 : 1;
  5680. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  5681. }
  5682. };
  5683. /**
  5684. * Check if the current value is a major value (for example when the step
  5685. * is DAY, a major value is each first day of the MONTH)
  5686. * @return {boolean} true if current date is major, else false.
  5687. */
  5688. TimeStep.prototype.isMajor = function() {
  5689. switch (this.scale) {
  5690. case TimeStep.SCALE.MILLISECOND:
  5691. return (this.current.getMilliseconds() == 0);
  5692. case TimeStep.SCALE.SECOND:
  5693. return (this.current.getSeconds() == 0);
  5694. case TimeStep.SCALE.MINUTE:
  5695. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  5696. // Note: this is no bug. Major label is equal for both minute and hour scale
  5697. case TimeStep.SCALE.HOUR:
  5698. return (this.current.getHours() == 0);
  5699. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5700. case TimeStep.SCALE.DAY:
  5701. return (this.current.getDate() == 1);
  5702. case TimeStep.SCALE.MONTH:
  5703. return (this.current.getMonth() == 0);
  5704. case TimeStep.SCALE.YEAR:
  5705. return false;
  5706. default:
  5707. return false;
  5708. }
  5709. };
  5710. /**
  5711. * Returns formatted text for the minor axislabel, depending on the current
  5712. * date and the scale. For example when scale is MINUTE, the current time is
  5713. * formatted as "hh:mm".
  5714. * @param {Date} [date] custom date. if not provided, current date is taken
  5715. */
  5716. TimeStep.prototype.getLabelMinor = function(date) {
  5717. if (date == undefined) {
  5718. date = this.current;
  5719. }
  5720. switch (this.scale) {
  5721. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  5722. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  5723. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  5724. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  5725. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  5726. case TimeStep.SCALE.DAY: return moment(date).format('D');
  5727. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  5728. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  5729. default: return '';
  5730. }
  5731. };
  5732. /**
  5733. * Returns formatted text for the major axis label, depending on the current
  5734. * date and the scale. For example when scale is MINUTE, the major scale is
  5735. * hours, and the hour will be formatted as "hh".
  5736. * @param {Date} [date] custom date. if not provided, current date is taken
  5737. */
  5738. TimeStep.prototype.getLabelMajor = function(date) {
  5739. if (date == undefined) {
  5740. date = this.current;
  5741. }
  5742. //noinspection FallthroughInSwitchStatementJS
  5743. switch (this.scale) {
  5744. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  5745. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  5746. case TimeStep.SCALE.MINUTE:
  5747. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  5748. case TimeStep.SCALE.WEEKDAY:
  5749. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  5750. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  5751. case TimeStep.SCALE.YEAR: return '';
  5752. default: return '';
  5753. }
  5754. };
  5755. /**
  5756. * @constructor Stack
  5757. * Stacks items on top of each other.
  5758. * @param {ItemSet} parent
  5759. * @param {Object} [options]
  5760. */
  5761. function Stack (parent, options) {
  5762. this.parent = parent;
  5763. this.options = options || {};
  5764. this.defaultOptions = {
  5765. order: function (a, b) {
  5766. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  5767. // Order: ranges over non-ranges, ranged ordered by width, and
  5768. // lastly ordered by start.
  5769. if (a instanceof ItemRange) {
  5770. if (b instanceof ItemRange) {
  5771. var aInt = (a.data.end - a.data.start);
  5772. var bInt = (b.data.end - b.data.start);
  5773. return (aInt - bInt) || (a.data.start - b.data.start);
  5774. }
  5775. else {
  5776. return -1;
  5777. }
  5778. }
  5779. else {
  5780. if (b instanceof ItemRange) {
  5781. return 1;
  5782. }
  5783. else {
  5784. return (a.data.start - b.data.start);
  5785. }
  5786. }
  5787. },
  5788. margin: {
  5789. item: 10
  5790. }
  5791. };
  5792. this.ordered = []; // ordered items
  5793. }
  5794. /**
  5795. * Set options for the stack
  5796. * @param {Object} options Available options:
  5797. * {ItemSet} parent
  5798. * {Number} margin
  5799. * {function} order Stacking order
  5800. */
  5801. Stack.prototype.setOptions = function setOptions (options) {
  5802. util.extend(this.options, options);
  5803. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  5804. };
  5805. /**
  5806. * Stack the items such that they don't overlap. The items will have a minimal
  5807. * distance equal to options.margin.item.
  5808. */
  5809. Stack.prototype.update = function update() {
  5810. this._order();
  5811. this._stack();
  5812. };
  5813. /**
  5814. * Order the items. The items are ordered by width first, and by left position
  5815. * second.
  5816. * If a custom order function has been provided via the options, then this will
  5817. * be used.
  5818. * @private
  5819. */
  5820. Stack.prototype._order = function _order () {
  5821. var items = this.parent.items;
  5822. if (!items) {
  5823. throw new Error('Cannot stack items: parent does not contain items');
  5824. }
  5825. // TODO: store the sorted items, to have less work later on
  5826. var ordered = [];
  5827. var index = 0;
  5828. // items is a map (no array)
  5829. util.forEach(items, function (item) {
  5830. if (item.visible) {
  5831. ordered[index] = item;
  5832. index++;
  5833. }
  5834. });
  5835. //if a customer stack order function exists, use it.
  5836. var order = this.options.order || this.defaultOptions.order;
  5837. if (!(typeof order === 'function')) {
  5838. throw new Error('Option order must be a function');
  5839. }
  5840. ordered.sort(order);
  5841. this.ordered = ordered;
  5842. };
  5843. /**
  5844. * Adjust vertical positions of the events such that they don't overlap each
  5845. * other.
  5846. * @private
  5847. */
  5848. Stack.prototype._stack = function _stack () {
  5849. var i,
  5850. iMax,
  5851. ordered = this.ordered,
  5852. options = this.options,
  5853. orientation = options.orientation || this.defaultOptions.orientation,
  5854. axisOnTop = (orientation == 'top'),
  5855. margin;
  5856. if (options.margin && options.margin.item !== undefined) {
  5857. margin = options.margin.item;
  5858. }
  5859. else {
  5860. margin = this.defaultOptions.margin.item
  5861. }
  5862. // calculate new, non-overlapping positions
  5863. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  5864. var item = ordered[i];
  5865. var collidingItem = null;
  5866. do {
  5867. // TODO: optimize checking for overlap. when there is a gap without items,
  5868. // you only need to check for items from the next item on, not from zero
  5869. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  5870. if (collidingItem != null) {
  5871. // There is a collision. Reposition the event above the colliding element
  5872. if (axisOnTop) {
  5873. item.top = collidingItem.top + collidingItem.height + margin;
  5874. }
  5875. else {
  5876. item.top = collidingItem.top - item.height - margin;
  5877. }
  5878. }
  5879. } while (collidingItem);
  5880. }
  5881. };
  5882. /**
  5883. * Check if the destiny position of given item overlaps with any
  5884. * of the other items from index itemStart to itemEnd.
  5885. * @param {Array} items Array with items
  5886. * @param {int} itemIndex Number of the item to be checked for overlap
  5887. * @param {int} itemStart First item to be checked.
  5888. * @param {int} itemEnd Last item to be checked.
  5889. * @return {Object | null} colliding item, or undefined when no collisions
  5890. * @param {Number} margin A minimum required margin.
  5891. * If margin is provided, the two items will be
  5892. * marked colliding when they overlap or
  5893. * when the margin between the two is smaller than
  5894. * the requested margin.
  5895. */
  5896. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  5897. itemStart, itemEnd, margin) {
  5898. var collision = this.collision;
  5899. // we loop from end to start, as we suppose that the chance of a
  5900. // collision is larger for items at the end, so check these first.
  5901. var a = items[itemIndex];
  5902. for (var i = itemEnd; i >= itemStart; i--) {
  5903. var b = items[i];
  5904. if (collision(a, b, margin)) {
  5905. if (i != itemIndex) {
  5906. return b;
  5907. }
  5908. }
  5909. }
  5910. return null;
  5911. };
  5912. /**
  5913. * Test if the two provided items collide
  5914. * The items must have parameters left, width, top, and height.
  5915. * @param {Component} a The first item
  5916. * @param {Component} b The second item
  5917. * @param {Number} margin A minimum required margin.
  5918. * If margin is provided, the two items will be
  5919. * marked colliding when they overlap or
  5920. * when the margin between the two is smaller than
  5921. * the requested margin.
  5922. * @return {boolean} true if a and b collide, else false
  5923. */
  5924. Stack.prototype.collision = function collision (a, b, margin) {
  5925. return ((a.left - margin) < (b.left + b.getWidth()) &&
  5926. (a.left + a.getWidth() + margin) > b.left &&
  5927. (a.top - margin) < (b.top + b.height) &&
  5928. (a.top + a.height + margin) > b.top);
  5929. };
  5930. /**
  5931. * @constructor Range
  5932. * A Range controls a numeric range with a start and end value.
  5933. * The Range adjusts the range based on mouse events or programmatic changes,
  5934. * and triggers events when the range is changing or has been changed.
  5935. * @param {Object} [options] See description at Range.setOptions
  5936. * @extends Controller
  5937. */
  5938. function Range(options) {
  5939. this.id = util.randomUUID();
  5940. this.start = null; // Number
  5941. this.end = null; // Number
  5942. this.options = options || {};
  5943. this.listeners = [];
  5944. this.setOptions(options);
  5945. }
  5946. /**
  5947. * Set options for the range controller
  5948. * @param {Object} options Available options:
  5949. * {Number} min Minimum value for start
  5950. * {Number} max Maximum value for end
  5951. * {Number} zoomMin Set a minimum value for
  5952. * (end - start).
  5953. * {Number} zoomMax Set a maximum value for
  5954. * (end - start).
  5955. */
  5956. Range.prototype.setOptions = function (options) {
  5957. util.extend(this.options, options);
  5958. // re-apply range with new limitations
  5959. if (this.start !== null && this.end !== null) {
  5960. this.setRange(this.start, this.end);
  5961. }
  5962. };
  5963. /**
  5964. * Add listeners for mouse and touch events to the component
  5965. * @param {Component} component
  5966. * @param {String} event Available events: 'move', 'zoom'
  5967. * @param {String} direction Available directions: 'horizontal', 'vertical'
  5968. */
  5969. Range.prototype.subscribe = function (component, event, direction) {
  5970. var me = this;
  5971. var listener;
  5972. if (direction != 'horizontal' && direction != 'vertical') {
  5973. throw new TypeError('Unknown direction "' + direction + '". ' +
  5974. 'Choose "horizontal" or "vertical".');
  5975. }
  5976. //noinspection FallthroughInSwitchStatementJS
  5977. if (event == 'move') {
  5978. listener = {
  5979. component: component,
  5980. event: event,
  5981. direction: direction,
  5982. callback: function (event) {
  5983. me._onMouseDown(event, listener);
  5984. },
  5985. params: {}
  5986. };
  5987. component.on('mousedown', listener.callback);
  5988. me.listeners.push(listener);
  5989. }
  5990. else if (event == 'zoom') {
  5991. listener = {
  5992. component: component,
  5993. event: event,
  5994. direction: direction,
  5995. callback: function (event) {
  5996. me._onMouseWheel(event, listener);
  5997. },
  5998. params: {}
  5999. };
  6000. component.on('mousewheel', listener.callback);
  6001. me.listeners.push(listener);
  6002. }
  6003. else {
  6004. throw new TypeError('Unknown event "' + event + '". ' +
  6005. 'Choose "move" or "zoom".');
  6006. }
  6007. };
  6008. /**
  6009. * Event handler
  6010. * @param {String} event name of the event, for example 'click', 'mousemove'
  6011. * @param {function} callback callback handler, invoked with the raw HTML Event
  6012. * as parameter.
  6013. */
  6014. Range.prototype.on = function (event, callback) {
  6015. events.addListener(this, event, callback);
  6016. };
  6017. /**
  6018. * Trigger an event
  6019. * @param {String} event name of the event, available events: 'rangechange',
  6020. * 'rangechanged'
  6021. * @private
  6022. */
  6023. Range.prototype._trigger = function (event) {
  6024. events.trigger(this, event, {
  6025. start: this.start,
  6026. end: this.end
  6027. });
  6028. };
  6029. /**
  6030. * Set a new start and end range
  6031. * @param {Number} [start]
  6032. * @param {Number} [end]
  6033. */
  6034. Range.prototype.setRange = function(start, end) {
  6035. var changed = this._applyRange(start, end);
  6036. if (changed) {
  6037. this._trigger('rangechange');
  6038. this._trigger('rangechanged');
  6039. }
  6040. };
  6041. /**
  6042. * Set a new start and end range. This method is the same as setRange, but
  6043. * does not trigger a range change and range changed event, and it returns
  6044. * true when the range is changed
  6045. * @param {Number} [start]
  6046. * @param {Number} [end]
  6047. * @return {Boolean} changed
  6048. * @private
  6049. */
  6050. Range.prototype._applyRange = function(start, end) {
  6051. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  6052. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  6053. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  6054. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  6055. diff;
  6056. // check for valid number
  6057. if (isNaN(newStart) || newStart === null) {
  6058. throw new Error('Invalid start "' + start + '"');
  6059. }
  6060. if (isNaN(newEnd) || newEnd === null) {
  6061. throw new Error('Invalid end "' + end + '"');
  6062. }
  6063. // prevent start < end
  6064. if (newEnd < newStart) {
  6065. newEnd = newStart;
  6066. }
  6067. // prevent start < min
  6068. if (min !== null) {
  6069. if (newStart < min) {
  6070. diff = (min - newStart);
  6071. newStart += diff;
  6072. newEnd += diff;
  6073. // prevent end > max
  6074. if (max != null) {
  6075. if (newEnd > max) {
  6076. newEnd = max;
  6077. }
  6078. }
  6079. }
  6080. }
  6081. // prevent end > max
  6082. if (max !== null) {
  6083. if (newEnd > max) {
  6084. diff = (newEnd - max);
  6085. newStart -= diff;
  6086. newEnd -= diff;
  6087. // prevent start < min
  6088. if (min != null) {
  6089. if (newStart < min) {
  6090. newStart = min;
  6091. }
  6092. }
  6093. }
  6094. }
  6095. // prevent (end-start) < zoomMin
  6096. if (this.options.zoomMin !== null) {
  6097. var zoomMin = parseFloat(this.options.zoomMin);
  6098. if (zoomMin < 0) {
  6099. zoomMin = 0;
  6100. }
  6101. if ((newEnd - newStart) < zoomMin) {
  6102. if ((this.end - this.start) === zoomMin) {
  6103. // ignore this action, we are already zoomed to the minimum
  6104. newStart = this.start;
  6105. newEnd = this.end;
  6106. }
  6107. else {
  6108. // zoom to the minimum
  6109. diff = (zoomMin - (newEnd - newStart));
  6110. newStart -= diff / 2;
  6111. newEnd += diff / 2;
  6112. }
  6113. }
  6114. }
  6115. // prevent (end-start) > zoomMax
  6116. if (this.options.zoomMax !== null) {
  6117. var zoomMax = parseFloat(this.options.zoomMax);
  6118. if (zoomMax < 0) {
  6119. zoomMax = 0;
  6120. }
  6121. if ((newEnd - newStart) > zoomMax) {
  6122. if ((this.end - this.start) === zoomMax) {
  6123. // ignore this action, we are already zoomed to the maximum
  6124. newStart = this.start;
  6125. newEnd = this.end;
  6126. }
  6127. else {
  6128. // zoom to the maximum
  6129. diff = ((newEnd - newStart) - zoomMax);
  6130. newStart += diff / 2;
  6131. newEnd -= diff / 2;
  6132. }
  6133. }
  6134. }
  6135. var changed = (this.start != newStart || this.end != newEnd);
  6136. this.start = newStart;
  6137. this.end = newEnd;
  6138. return changed;
  6139. };
  6140. /**
  6141. * Retrieve the current range.
  6142. * @return {Object} An object with start and end properties
  6143. */
  6144. Range.prototype.getRange = function() {
  6145. return {
  6146. start: this.start,
  6147. end: this.end
  6148. };
  6149. };
  6150. /**
  6151. * Calculate the conversion offset and factor for current range, based on
  6152. * the provided width
  6153. * @param {Number} width
  6154. * @returns {{offset: number, factor: number}} conversion
  6155. */
  6156. Range.prototype.conversion = function (width) {
  6157. var start = this.start;
  6158. var end = this.end;
  6159. return Range.conversion(this.start, this.end, width);
  6160. };
  6161. /**
  6162. * Static method to calculate the conversion offset and factor for a range,
  6163. * based on the provided start, end, and width
  6164. * @param {Number} start
  6165. * @param {Number} end
  6166. * @param {Number} width
  6167. * @returns {{offset: number, factor: number}} conversion
  6168. */
  6169. Range.conversion = function (start, end, width) {
  6170. if (width != 0 && (end - start != 0)) {
  6171. return {
  6172. offset: start,
  6173. factor: width / (end - start)
  6174. }
  6175. }
  6176. else {
  6177. return {
  6178. offset: 0,
  6179. factor: 1
  6180. };
  6181. }
  6182. };
  6183. /**
  6184. * Start moving horizontally or vertically
  6185. * @param {Event} event
  6186. * @param {Object} listener Listener containing the component and params
  6187. * @private
  6188. */
  6189. Range.prototype._onMouseDown = function(event, listener) {
  6190. event = event || window.event;
  6191. var params = listener.params;
  6192. // only react on left mouse button down
  6193. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  6194. if (!leftButtonDown) {
  6195. return;
  6196. }
  6197. // get mouse position
  6198. params.mouseX = util.getPageX(event);
  6199. params.mouseY = util.getPageY(event);
  6200. params.previousLeft = 0;
  6201. params.previousOffset = 0;
  6202. params.moved = false;
  6203. params.start = this.start;
  6204. params.end = this.end;
  6205. var frame = listener.component.frame;
  6206. if (frame) {
  6207. frame.style.cursor = 'move';
  6208. }
  6209. // add event listeners to handle moving the contents
  6210. // we store the function onmousemove and onmouseup in the timeaxis,
  6211. // so we can remove the eventlisteners lateron in the function onmouseup
  6212. var me = this;
  6213. if (!params.onMouseMove) {
  6214. params.onMouseMove = function (event) {
  6215. me._onMouseMove(event, listener);
  6216. };
  6217. util.addEventListener(document, "mousemove", params.onMouseMove);
  6218. }
  6219. if (!params.onMouseUp) {
  6220. params.onMouseUp = function (event) {
  6221. me._onMouseUp(event, listener);
  6222. };
  6223. util.addEventListener(document, "mouseup", params.onMouseUp);
  6224. }
  6225. util.preventDefault(event);
  6226. };
  6227. /**
  6228. * Perform moving operating.
  6229. * This function activated from within the funcion TimeAxis._onMouseDown().
  6230. * @param {Event} event
  6231. * @param {Object} listener
  6232. * @private
  6233. */
  6234. Range.prototype._onMouseMove = function (event, listener) {
  6235. event = event || window.event;
  6236. var params = listener.params;
  6237. // calculate change in mouse position
  6238. var mouseX = util.getPageX(event);
  6239. var mouseY = util.getPageY(event);
  6240. if (params.mouseX == undefined) {
  6241. params.mouseX = mouseX;
  6242. }
  6243. if (params.mouseY == undefined) {
  6244. params.mouseY = mouseY;
  6245. }
  6246. var diffX = mouseX - params.mouseX;
  6247. var diffY = mouseY - params.mouseY;
  6248. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  6249. // if mouse movement is big enough, register it as a "moved" event
  6250. if (Math.abs(diff) >= 1) {
  6251. params.moved = true;
  6252. }
  6253. var interval = (params.end - params.start);
  6254. var width = (listener.direction == 'horizontal') ?
  6255. listener.component.width : listener.component.height;
  6256. var diffRange = -diff / width * interval;
  6257. this._applyRange(params.start + diffRange, params.end + diffRange);
  6258. // fire a rangechange event
  6259. this._trigger('rangechange');
  6260. util.preventDefault(event);
  6261. };
  6262. /**
  6263. * Stop moving operating.
  6264. * This function activated from within the function Range._onMouseDown().
  6265. * @param {event} event
  6266. * @param {Object} listener
  6267. * @private
  6268. */
  6269. Range.prototype._onMouseUp = function (event, listener) {
  6270. event = event || window.event;
  6271. var params = listener.params;
  6272. if (listener.component.frame) {
  6273. listener.component.frame.style.cursor = 'auto';
  6274. }
  6275. // remove event listeners here, important for Safari
  6276. if (params.onMouseMove) {
  6277. util.removeEventListener(document, "mousemove", params.onMouseMove);
  6278. params.onMouseMove = null;
  6279. }
  6280. if (params.onMouseUp) {
  6281. util.removeEventListener(document, "mouseup", params.onMouseUp);
  6282. params.onMouseUp = null;
  6283. }
  6284. //util.preventDefault(event);
  6285. if (params.moved) {
  6286. // fire a rangechanged event
  6287. this._trigger('rangechanged');
  6288. }
  6289. };
  6290. /**
  6291. * Event handler for mouse wheel event, used to zoom
  6292. * Code from http://adomas.org/javascript-mouse-wheel/
  6293. * @param {Event} event
  6294. * @param {Object} listener
  6295. * @private
  6296. */
  6297. Range.prototype._onMouseWheel = function(event, listener) {
  6298. event = event || window.event;
  6299. // retrieve delta
  6300. var delta = 0;
  6301. if (event.wheelDelta) { /* IE/Opera. */
  6302. delta = event.wheelDelta / 120;
  6303. } else if (event.detail) { /* Mozilla case. */
  6304. // In Mozilla, sign of delta is different than in IE.
  6305. // Also, delta is multiple of 3.
  6306. delta = -event.detail / 3;
  6307. }
  6308. // If delta is nonzero, handle it.
  6309. // Basically, delta is now positive if wheel was scrolled up,
  6310. // and negative, if wheel was scrolled down.
  6311. if (delta) {
  6312. var me = this;
  6313. var zoom = function () {
  6314. // perform the zoom action. Delta is normally 1 or -1
  6315. var zoomFactor = delta / 5.0;
  6316. var zoomAround = null;
  6317. var frame = listener.component.frame;
  6318. if (frame) {
  6319. var size, conversion;
  6320. if (listener.direction == 'horizontal') {
  6321. size = listener.component.width;
  6322. conversion = me.conversion(size);
  6323. var frameLeft = util.getAbsoluteLeft(frame);
  6324. var mouseX = util.getPageX(event);
  6325. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  6326. }
  6327. else {
  6328. size = listener.component.height;
  6329. conversion = me.conversion(size);
  6330. var frameTop = util.getAbsoluteTop(frame);
  6331. var mouseY = util.getPageY(event);
  6332. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  6333. }
  6334. }
  6335. me.zoom(zoomFactor, zoomAround);
  6336. };
  6337. zoom();
  6338. }
  6339. // Prevent default actions caused by mouse wheel.
  6340. // That might be ugly, but we handle scrolls somehow
  6341. // anyway, so don't bother here...
  6342. util.preventDefault(event);
  6343. };
  6344. /**
  6345. * Zoom the range the given zoomfactor in or out. Start and end date will
  6346. * be adjusted, and the timeline will be redrawn. You can optionally give a
  6347. * date around which to zoom.
  6348. * For example, try zoomfactor = 0.1 or -0.1
  6349. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  6350. * negative value will zoom out
  6351. * @param {Number} zoomAround Value around which will be zoomed. Optional
  6352. */
  6353. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  6354. // if zoomAroundDate is not provided, take it half between start Date and end Date
  6355. if (zoomAround == null) {
  6356. zoomAround = (this.start + this.end) / 2;
  6357. }
  6358. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  6359. // result in a start>=end )
  6360. if (zoomFactor >= 1) {
  6361. zoomFactor = 0.9;
  6362. }
  6363. if (zoomFactor <= -1) {
  6364. zoomFactor = -0.9;
  6365. }
  6366. // adjust a negative factor such that zooming in with 0.1 equals zooming
  6367. // out with a factor -0.1
  6368. if (zoomFactor < 0) {
  6369. zoomFactor = zoomFactor / (1 + zoomFactor);
  6370. }
  6371. // zoom start and end relative to the zoomAround value
  6372. var startDiff = (this.start - zoomAround);
  6373. var endDiff = (this.end - zoomAround);
  6374. // calculate new start and end
  6375. var newStart = this.start - startDiff * zoomFactor;
  6376. var newEnd = this.end - endDiff * zoomFactor;
  6377. this.setRange(newStart, newEnd);
  6378. };
  6379. /**
  6380. * Move the range with a given factor to the left or right. Start and end
  6381. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  6382. * @param {Number} moveFactor Moving amount. Positive value will move right,
  6383. * negative value will move left
  6384. */
  6385. Range.prototype.move = function(moveFactor) {
  6386. // zoom start Date and end Date relative to the zoomAroundDate
  6387. var diff = (this.end - this.start);
  6388. // apply new values
  6389. var newStart = this.start + diff * moveFactor;
  6390. var newEnd = this.end + diff * moveFactor;
  6391. // TODO: reckon with min and max range
  6392. this.start = newStart;
  6393. this.end = newEnd;
  6394. };
  6395. /**
  6396. * Move the range to a new center point
  6397. * @param {Number} moveTo New center point of the range
  6398. */
  6399. Range.prototype.moveTo = function(moveTo) {
  6400. var center = (this.start + this.end) / 2;
  6401. var diff = center - moveTo;
  6402. // calculate new start and end
  6403. var newStart = this.start - diff;
  6404. var newEnd = this.end - diff;
  6405. this.setRange(newStart, newEnd);
  6406. }
  6407. /**
  6408. * @constructor Controller
  6409. *
  6410. * A Controller controls the reflows and repaints of all visual components
  6411. */
  6412. function Controller () {
  6413. this.id = util.randomUUID();
  6414. this.components = {};
  6415. this.repaintTimer = undefined;
  6416. this.reflowTimer = undefined;
  6417. }
  6418. /**
  6419. * Add a component to the controller
  6420. * @param {Component} component
  6421. */
  6422. Controller.prototype.add = function add(component) {
  6423. // validate the component
  6424. if (component.id == undefined) {
  6425. throw new Error('Component has no field id');
  6426. }
  6427. if (!(component instanceof Component) && !(component instanceof Controller)) {
  6428. throw new TypeError('Component must be an instance of ' +
  6429. 'prototype Component or Controller');
  6430. }
  6431. // add the component
  6432. component.controller = this;
  6433. this.components[component.id] = component;
  6434. };
  6435. /**
  6436. * Remove a component from the controller
  6437. * @param {Component | String} component
  6438. */
  6439. Controller.prototype.remove = function remove(component) {
  6440. var id;
  6441. for (id in this.components) {
  6442. if (this.components.hasOwnProperty(id)) {
  6443. if (id == component || this.components[id] == component) {
  6444. break;
  6445. }
  6446. }
  6447. }
  6448. if (id) {
  6449. delete this.components[id];
  6450. }
  6451. };
  6452. /**
  6453. * Request a reflow. The controller will schedule a reflow
  6454. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  6455. * is false.
  6456. */
  6457. Controller.prototype.requestReflow = function requestReflow(force) {
  6458. if (force) {
  6459. this.reflow();
  6460. }
  6461. else {
  6462. if (!this.reflowTimer) {
  6463. var me = this;
  6464. this.reflowTimer = setTimeout(function () {
  6465. me.reflowTimer = undefined;
  6466. me.reflow();
  6467. }, 0);
  6468. }
  6469. }
  6470. };
  6471. /**
  6472. * Request a repaint. The controller will schedule a repaint
  6473. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  6474. * is false.
  6475. */
  6476. Controller.prototype.requestRepaint = function requestRepaint(force) {
  6477. if (force) {
  6478. this.repaint();
  6479. }
  6480. else {
  6481. if (!this.repaintTimer) {
  6482. var me = this;
  6483. this.repaintTimer = setTimeout(function () {
  6484. me.repaintTimer = undefined;
  6485. me.repaint();
  6486. }, 0);
  6487. }
  6488. }
  6489. };
  6490. /**
  6491. * Repaint all components
  6492. */
  6493. Controller.prototype.repaint = function repaint() {
  6494. var changed = false;
  6495. // cancel any running repaint request
  6496. if (this.repaintTimer) {
  6497. clearTimeout(this.repaintTimer);
  6498. this.repaintTimer = undefined;
  6499. }
  6500. var done = {};
  6501. function repaint(component, id) {
  6502. if (!(id in done)) {
  6503. // first repaint the components on which this component is dependent
  6504. if (component.depends) {
  6505. component.depends.forEach(function (dep) {
  6506. repaint(dep, dep.id);
  6507. });
  6508. }
  6509. if (component.parent) {
  6510. repaint(component.parent, component.parent.id);
  6511. }
  6512. // repaint the component itself and mark as done
  6513. changed = component.repaint() || changed;
  6514. done[id] = true;
  6515. }
  6516. }
  6517. util.forEach(this.components, repaint);
  6518. // immediately reflow when needed
  6519. if (changed) {
  6520. this.reflow();
  6521. }
  6522. // TODO: limit the number of nested reflows/repaints, prevent loop
  6523. };
  6524. /**
  6525. * Reflow all components
  6526. */
  6527. Controller.prototype.reflow = function reflow() {
  6528. var resized = false;
  6529. // cancel any running repaint request
  6530. if (this.reflowTimer) {
  6531. clearTimeout(this.reflowTimer);
  6532. this.reflowTimer = undefined;
  6533. }
  6534. var done = {};
  6535. function reflow(component, id) {
  6536. if (!(id in done)) {
  6537. // first reflow the components on which this component is dependent
  6538. if (component.depends) {
  6539. component.depends.forEach(function (dep) {
  6540. reflow(dep, dep.id);
  6541. });
  6542. }
  6543. if (component.parent) {
  6544. reflow(component.parent, component.parent.id);
  6545. }
  6546. // reflow the component itself and mark as done
  6547. resized = component.reflow() || resized;
  6548. done[id] = true;
  6549. }
  6550. }
  6551. util.forEach(this.components, reflow);
  6552. // immediately repaint when needed
  6553. if (resized) {
  6554. this.repaint();
  6555. }
  6556. // TODO: limit the number of nested reflows/repaints, prevent loop
  6557. };
  6558. /**
  6559. * Prototype for visual components
  6560. */
  6561. function Component () {
  6562. this.id = null;
  6563. this.parent = null;
  6564. this.depends = null;
  6565. this.controller = null;
  6566. this.options = null;
  6567. this.frame = null; // main DOM element
  6568. this.top = 0;
  6569. this.left = 0;
  6570. this.width = 0;
  6571. this.height = 0;
  6572. }
  6573. /**
  6574. * Set parameters for the frame. Parameters will be merged in current parameter
  6575. * set.
  6576. * @param {Object} options Available parameters:
  6577. * {String | function} [className]
  6578. * {EventBus} [eventBus]
  6579. * {String | Number | function} [left]
  6580. * {String | Number | function} [top]
  6581. * {String | Number | function} [width]
  6582. * {String | Number | function} [height]
  6583. */
  6584. Component.prototype.setOptions = function setOptions(options) {
  6585. if (options) {
  6586. util.extend(this.options, options);
  6587. if (this.controller) {
  6588. this.requestRepaint();
  6589. this.requestReflow();
  6590. }
  6591. }
  6592. };
  6593. /**
  6594. * Get an option value by name
  6595. * The function will first check this.options object, and else will check
  6596. * this.defaultOptions.
  6597. * @param {String} name
  6598. * @return {*} value
  6599. */
  6600. Component.prototype.getOption = function getOption(name) {
  6601. var value;
  6602. if (this.options) {
  6603. value = this.options[name];
  6604. }
  6605. if (value === undefined && this.defaultOptions) {
  6606. value = this.defaultOptions[name];
  6607. }
  6608. return value;
  6609. };
  6610. /**
  6611. * Get the container element of the component, which can be used by a child to
  6612. * add its own widgets. Not all components do have a container for childs, in
  6613. * that case null is returned.
  6614. * @returns {HTMLElement | null} container
  6615. */
  6616. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  6617. Component.prototype.getContainer = function getContainer() {
  6618. // should be implemented by the component
  6619. return null;
  6620. };
  6621. /**
  6622. * Get the frame element of the component, the outer HTML DOM element.
  6623. * @returns {HTMLElement | null} frame
  6624. */
  6625. Component.prototype.getFrame = function getFrame() {
  6626. return this.frame;
  6627. };
  6628. /**
  6629. * Repaint the component
  6630. * @return {Boolean} changed
  6631. */
  6632. Component.prototype.repaint = function repaint() {
  6633. // should be implemented by the component
  6634. return false;
  6635. };
  6636. /**
  6637. * Reflow the component
  6638. * @return {Boolean} resized
  6639. */
  6640. Component.prototype.reflow = function reflow() {
  6641. // should be implemented by the component
  6642. return false;
  6643. };
  6644. /**
  6645. * Hide the component from the DOM
  6646. * @return {Boolean} changed
  6647. */
  6648. Component.prototype.hide = function hide() {
  6649. if (this.frame && this.frame.parentNode) {
  6650. this.frame.parentNode.removeChild(this.frame);
  6651. return true;
  6652. }
  6653. else {
  6654. return false;
  6655. }
  6656. };
  6657. /**
  6658. * Show the component in the DOM (when not already visible).
  6659. * A repaint will be executed when the component is not visible
  6660. * @return {Boolean} changed
  6661. */
  6662. Component.prototype.show = function show() {
  6663. if (!this.frame || !this.frame.parentNode) {
  6664. return this.repaint();
  6665. }
  6666. else {
  6667. return false;
  6668. }
  6669. };
  6670. /**
  6671. * Request a repaint. The controller will schedule a repaint
  6672. */
  6673. Component.prototype.requestRepaint = function requestRepaint() {
  6674. if (this.controller) {
  6675. this.controller.requestRepaint();
  6676. }
  6677. else {
  6678. throw new Error('Cannot request a repaint: no controller configured');
  6679. // TODO: just do a repaint when no parent is configured?
  6680. }
  6681. };
  6682. /**
  6683. * Request a reflow. The controller will schedule a reflow
  6684. */
  6685. Component.prototype.requestReflow = function requestReflow() {
  6686. if (this.controller) {
  6687. this.controller.requestReflow();
  6688. }
  6689. else {
  6690. throw new Error('Cannot request a reflow: no controller configured');
  6691. // TODO: just do a reflow when no parent is configured?
  6692. }
  6693. };
  6694. /**
  6695. * A panel can contain components
  6696. * @param {Component} [parent]
  6697. * @param {Component[]} [depends] Components on which this components depends
  6698. * (except for the parent)
  6699. * @param {Object} [options] Available parameters:
  6700. * {String | Number | function} [left]
  6701. * {String | Number | function} [top]
  6702. * {String | Number | function} [width]
  6703. * {String | Number | function} [height]
  6704. * {String | function} [className]
  6705. * @constructor Panel
  6706. * @extends Component
  6707. */
  6708. function Panel(parent, depends, options) {
  6709. this.id = util.randomUUID();
  6710. this.parent = parent;
  6711. this.depends = depends;
  6712. this.options = options || {};
  6713. }
  6714. Panel.prototype = new Component();
  6715. /**
  6716. * Set options. Will extend the current options.
  6717. * @param {Object} [options] Available parameters:
  6718. * {String | function} [className]
  6719. * {String | Number | function} [left]
  6720. * {String | Number | function} [top]
  6721. * {String | Number | function} [width]
  6722. * {String | Number | function} [height]
  6723. */
  6724. Panel.prototype.setOptions = Component.prototype.setOptions;
  6725. /**
  6726. * Get the container element of the panel, which can be used by a child to
  6727. * add its own widgets.
  6728. * @returns {HTMLElement} container
  6729. */
  6730. Panel.prototype.getContainer = function () {
  6731. return this.frame;
  6732. };
  6733. /**
  6734. * Repaint the component
  6735. * @return {Boolean} changed
  6736. */
  6737. Panel.prototype.repaint = function () {
  6738. var changed = 0,
  6739. update = util.updateProperty,
  6740. asSize = util.option.asSize,
  6741. options = this.options,
  6742. frame = this.frame;
  6743. if (!frame) {
  6744. frame = document.createElement('div');
  6745. frame.className = 'panel';
  6746. var className = options.className;
  6747. if (className) {
  6748. if (typeof className == 'function') {
  6749. util.addClassName(frame, String(className()));
  6750. }
  6751. else {
  6752. util.addClassName(frame, String(className));
  6753. }
  6754. }
  6755. this.frame = frame;
  6756. changed += 1;
  6757. }
  6758. if (!frame.parentNode) {
  6759. if (!this.parent) {
  6760. throw new Error('Cannot repaint panel: no parent attached');
  6761. }
  6762. var parentContainer = this.parent.getContainer();
  6763. if (!parentContainer) {
  6764. throw new Error('Cannot repaint panel: parent has no container element');
  6765. }
  6766. parentContainer.appendChild(frame);
  6767. changed += 1;
  6768. }
  6769. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6770. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6771. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6772. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  6773. return (changed > 0);
  6774. };
  6775. /**
  6776. * Reflow the component
  6777. * @return {Boolean} resized
  6778. */
  6779. Panel.prototype.reflow = function () {
  6780. var changed = 0,
  6781. update = util.updateProperty,
  6782. frame = this.frame;
  6783. if (frame) {
  6784. changed += update(this, 'top', frame.offsetTop);
  6785. changed += update(this, 'left', frame.offsetLeft);
  6786. changed += update(this, 'width', frame.offsetWidth);
  6787. changed += update(this, 'height', frame.offsetHeight);
  6788. }
  6789. else {
  6790. changed += 1;
  6791. }
  6792. return (changed > 0);
  6793. };
  6794. /**
  6795. * A root panel can hold components. The root panel must be initialized with
  6796. * a DOM element as container.
  6797. * @param {HTMLElement} container
  6798. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  6799. * @constructor RootPanel
  6800. * @extends Panel
  6801. */
  6802. function RootPanel(container, options) {
  6803. this.id = util.randomUUID();
  6804. this.container = container;
  6805. this.options = options || {};
  6806. this.defaultOptions = {
  6807. autoResize: true
  6808. };
  6809. this.listeners = {}; // event listeners
  6810. }
  6811. RootPanel.prototype = new Panel();
  6812. /**
  6813. * Set options. Will extend the current options.
  6814. * @param {Object} [options] Available parameters:
  6815. * {String | function} [className]
  6816. * {String | Number | function} [left]
  6817. * {String | Number | function} [top]
  6818. * {String | Number | function} [width]
  6819. * {String | Number | function} [height]
  6820. * {Boolean | function} [autoResize]
  6821. */
  6822. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  6823. /**
  6824. * Repaint the component
  6825. * @return {Boolean} changed
  6826. */
  6827. RootPanel.prototype.repaint = function () {
  6828. var changed = 0,
  6829. update = util.updateProperty,
  6830. asSize = util.option.asSize,
  6831. options = this.options,
  6832. frame = this.frame;
  6833. if (!frame) {
  6834. frame = document.createElement('div');
  6835. frame.className = 'vis timeline rootpanel';
  6836. var className = options.className;
  6837. if (className) {
  6838. util.addClassName(frame, util.option.asString(className));
  6839. }
  6840. this.frame = frame;
  6841. changed += 1;
  6842. }
  6843. if (!frame.parentNode) {
  6844. if (!this.container) {
  6845. throw new Error('Cannot repaint root panel: no container attached');
  6846. }
  6847. this.container.appendChild(frame);
  6848. changed += 1;
  6849. }
  6850. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6851. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6852. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6853. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  6854. this._updateEventEmitters();
  6855. this._updateWatch();
  6856. return (changed > 0);
  6857. };
  6858. /**
  6859. * Reflow the component
  6860. * @return {Boolean} resized
  6861. */
  6862. RootPanel.prototype.reflow = function () {
  6863. var changed = 0,
  6864. update = util.updateProperty,
  6865. frame = this.frame;
  6866. if (frame) {
  6867. changed += update(this, 'top', frame.offsetTop);
  6868. changed += update(this, 'left', frame.offsetLeft);
  6869. changed += update(this, 'width', frame.offsetWidth);
  6870. changed += update(this, 'height', frame.offsetHeight);
  6871. }
  6872. else {
  6873. changed += 1;
  6874. }
  6875. return (changed > 0);
  6876. };
  6877. /**
  6878. * Update watching for resize, depending on the current option
  6879. * @private
  6880. */
  6881. RootPanel.prototype._updateWatch = function () {
  6882. var autoResize = this.getOption('autoResize');
  6883. if (autoResize) {
  6884. this._watch();
  6885. }
  6886. else {
  6887. this._unwatch();
  6888. }
  6889. };
  6890. /**
  6891. * Watch for changes in the size of the frame. On resize, the Panel will
  6892. * automatically redraw itself.
  6893. * @private
  6894. */
  6895. RootPanel.prototype._watch = function () {
  6896. var me = this;
  6897. this._unwatch();
  6898. var checkSize = function () {
  6899. var autoResize = me.getOption('autoResize');
  6900. if (!autoResize) {
  6901. // stop watching when the option autoResize is changed to false
  6902. me._unwatch();
  6903. return;
  6904. }
  6905. if (me.frame) {
  6906. // check whether the frame is resized
  6907. if ((me.frame.clientWidth != me.width) ||
  6908. (me.frame.clientHeight != me.height)) {
  6909. me.requestReflow();
  6910. }
  6911. }
  6912. };
  6913. // TODO: automatically cleanup the event listener when the frame is deleted
  6914. util.addEventListener(window, 'resize', checkSize);
  6915. this.watchTimer = setInterval(checkSize, 1000);
  6916. };
  6917. /**
  6918. * Stop watching for a resize of the frame.
  6919. * @private
  6920. */
  6921. RootPanel.prototype._unwatch = function () {
  6922. if (this.watchTimer) {
  6923. clearInterval(this.watchTimer);
  6924. this.watchTimer = undefined;
  6925. }
  6926. // TODO: remove event listener on window.resize
  6927. };
  6928. /**
  6929. * Event handler
  6930. * @param {String} event name of the event, for example 'click', 'mousemove'
  6931. * @param {function} callback callback handler, invoked with the raw HTML Event
  6932. * as parameter.
  6933. */
  6934. RootPanel.prototype.on = function (event, callback) {
  6935. // register the listener at this component
  6936. var arr = this.listeners[event];
  6937. if (!arr) {
  6938. arr = [];
  6939. this.listeners[event] = arr;
  6940. }
  6941. arr.push(callback);
  6942. this._updateEventEmitters();
  6943. };
  6944. /**
  6945. * Update the event listeners for all event emitters
  6946. * @private
  6947. */
  6948. RootPanel.prototype._updateEventEmitters = function () {
  6949. if (this.listeners) {
  6950. var me = this;
  6951. util.forEach(this.listeners, function (listeners, event) {
  6952. if (!me.emitters) {
  6953. me.emitters = {};
  6954. }
  6955. if (!(event in me.emitters)) {
  6956. // create event
  6957. var frame = me.frame;
  6958. if (frame) {
  6959. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  6960. var callback = function(event) {
  6961. listeners.forEach(function (listener) {
  6962. // TODO: filter on event target!
  6963. listener(event);
  6964. });
  6965. };
  6966. me.emitters[event] = callback;
  6967. util.addEventListener(frame, event, callback);
  6968. }
  6969. }
  6970. });
  6971. // TODO: be able to delete event listeners
  6972. // TODO: be able to move event listeners to a parent when available
  6973. }
  6974. };
  6975. /**
  6976. * A horizontal time axis
  6977. * @param {Component} parent
  6978. * @param {Component[]} [depends] Components on which this components depends
  6979. * (except for the parent)
  6980. * @param {Object} [options] See TimeAxis.setOptions for the available
  6981. * options.
  6982. * @constructor TimeAxis
  6983. * @extends Component
  6984. */
  6985. function TimeAxis (parent, depends, options) {
  6986. this.id = util.randomUUID();
  6987. this.parent = parent;
  6988. this.depends = depends;
  6989. this.dom = {
  6990. majorLines: [],
  6991. majorTexts: [],
  6992. minorLines: [],
  6993. minorTexts: [],
  6994. redundant: {
  6995. majorLines: [],
  6996. majorTexts: [],
  6997. minorLines: [],
  6998. minorTexts: []
  6999. }
  7000. };
  7001. this.props = {
  7002. range: {
  7003. start: 0,
  7004. end: 0,
  7005. minimumStep: 0
  7006. },
  7007. lineTop: 0
  7008. };
  7009. this.options = options || {};
  7010. this.defaultOptions = {
  7011. orientation: 'bottom', // supported: 'top', 'bottom'
  7012. // TODO: implement timeaxis orientations 'left' and 'right'
  7013. showMinorLabels: true,
  7014. showMajorLabels: true
  7015. };
  7016. this.conversion = null;
  7017. this.range = null;
  7018. }
  7019. TimeAxis.prototype = new Component();
  7020. // TODO: comment options
  7021. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  7022. /**
  7023. * Set a range (start and end)
  7024. * @param {Range | Object} range A Range or an object containing start and end.
  7025. */
  7026. TimeAxis.prototype.setRange = function (range) {
  7027. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  7028. throw new TypeError('Range must be an instance of Range, ' +
  7029. 'or an object containing start and end.');
  7030. }
  7031. this.range = range;
  7032. };
  7033. /**
  7034. * Convert a position on screen (pixels) to a datetime
  7035. * @param {int} x Position on the screen in pixels
  7036. * @return {Date} time The datetime the corresponds with given position x
  7037. */
  7038. TimeAxis.prototype.toTime = function(x) {
  7039. var conversion = this.conversion;
  7040. return new Date(x / conversion.factor + conversion.offset);
  7041. };
  7042. /**
  7043. * Convert a datetime (Date object) into a position on the screen
  7044. * @param {Date} time A date
  7045. * @return {int} x The position on the screen in pixels which corresponds
  7046. * with the given date.
  7047. * @private
  7048. */
  7049. TimeAxis.prototype.toScreen = function(time) {
  7050. var conversion = this.conversion;
  7051. return (time.valueOf() - conversion.offset) * conversion.factor;
  7052. };
  7053. /**
  7054. * Repaint the component
  7055. * @return {Boolean} changed
  7056. */
  7057. TimeAxis.prototype.repaint = function () {
  7058. var changed = 0,
  7059. update = util.updateProperty,
  7060. asSize = util.option.asSize,
  7061. options = this.options,
  7062. orientation = this.getOption('orientation'),
  7063. props = this.props,
  7064. step = this.step;
  7065. var frame = this.frame;
  7066. if (!frame) {
  7067. frame = document.createElement('div');
  7068. this.frame = frame;
  7069. changed += 1;
  7070. }
  7071. frame.className = 'axis ' + orientation;
  7072. // TODO: custom className?
  7073. if (!frame.parentNode) {
  7074. if (!this.parent) {
  7075. throw new Error('Cannot repaint time axis: no parent attached');
  7076. }
  7077. var parentContainer = this.parent.getContainer();
  7078. if (!parentContainer) {
  7079. throw new Error('Cannot repaint time axis: parent has no container element');
  7080. }
  7081. parentContainer.appendChild(frame);
  7082. changed += 1;
  7083. }
  7084. var parent = frame.parentNode;
  7085. if (parent) {
  7086. var beforeChild = frame.nextSibling;
  7087. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  7088. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  7089. (this.props.parentHeight - this.height) + 'px' :
  7090. '0px';
  7091. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  7092. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7093. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7094. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  7095. // get characters width and height
  7096. this._repaintMeasureChars();
  7097. if (this.step) {
  7098. this._repaintStart();
  7099. step.first();
  7100. var xFirstMajorLabel = undefined;
  7101. var max = 0;
  7102. while (step.hasNext() && max < 1000) {
  7103. max++;
  7104. var cur = step.getCurrent(),
  7105. x = this.toScreen(cur),
  7106. isMajor = step.isMajor();
  7107. // TODO: lines must have a width, such that we can create css backgrounds
  7108. if (this.getOption('showMinorLabels')) {
  7109. this._repaintMinorText(x, step.getLabelMinor());
  7110. }
  7111. if (isMajor && this.getOption('showMajorLabels')) {
  7112. if (x > 0) {
  7113. if (xFirstMajorLabel == undefined) {
  7114. xFirstMajorLabel = x;
  7115. }
  7116. this._repaintMajorText(x, step.getLabelMajor());
  7117. }
  7118. this._repaintMajorLine(x);
  7119. }
  7120. else {
  7121. this._repaintMinorLine(x);
  7122. }
  7123. step.next();
  7124. }
  7125. // create a major label on the left when needed
  7126. if (this.getOption('showMajorLabels')) {
  7127. var leftTime = this.toTime(0),
  7128. leftText = step.getLabelMajor(leftTime),
  7129. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  7130. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  7131. this._repaintMajorText(0, leftText);
  7132. }
  7133. }
  7134. this._repaintEnd();
  7135. }
  7136. this._repaintLine();
  7137. // put frame online again
  7138. if (beforeChild) {
  7139. parent.insertBefore(frame, beforeChild);
  7140. }
  7141. else {
  7142. parent.appendChild(frame)
  7143. }
  7144. }
  7145. return (changed > 0);
  7146. };
  7147. /**
  7148. * Start a repaint. Move all DOM elements to a redundant list, where they
  7149. * can be picked for re-use, or can be cleaned up in the end
  7150. * @private
  7151. */
  7152. TimeAxis.prototype._repaintStart = function () {
  7153. var dom = this.dom,
  7154. redundant = dom.redundant;
  7155. redundant.majorLines = dom.majorLines;
  7156. redundant.majorTexts = dom.majorTexts;
  7157. redundant.minorLines = dom.minorLines;
  7158. redundant.minorTexts = dom.minorTexts;
  7159. dom.majorLines = [];
  7160. dom.majorTexts = [];
  7161. dom.minorLines = [];
  7162. dom.minorTexts = [];
  7163. };
  7164. /**
  7165. * End a repaint. Cleanup leftover DOM elements in the redundant list
  7166. * @private
  7167. */
  7168. TimeAxis.prototype._repaintEnd = function () {
  7169. util.forEach(this.dom.redundant, function (arr) {
  7170. while (arr.length) {
  7171. var elem = arr.pop();
  7172. if (elem && elem.parentNode) {
  7173. elem.parentNode.removeChild(elem);
  7174. }
  7175. }
  7176. });
  7177. };
  7178. /**
  7179. * Create a minor label for the axis at position x
  7180. * @param {Number} x
  7181. * @param {String} text
  7182. * @private
  7183. */
  7184. TimeAxis.prototype._repaintMinorText = function (x, text) {
  7185. // reuse redundant label
  7186. var label = this.dom.redundant.minorTexts.shift();
  7187. if (!label) {
  7188. // create new label
  7189. var content = document.createTextNode('');
  7190. label = document.createElement('div');
  7191. label.appendChild(content);
  7192. label.className = 'text minor';
  7193. this.frame.appendChild(label);
  7194. }
  7195. this.dom.minorTexts.push(label);
  7196. label.childNodes[0].nodeValue = text;
  7197. label.style.left = x + 'px';
  7198. label.style.top = this.props.minorLabelTop + 'px';
  7199. //label.title = title; // TODO: this is a heavy operation
  7200. };
  7201. /**
  7202. * Create a Major label for the axis at position x
  7203. * @param {Number} x
  7204. * @param {String} text
  7205. * @private
  7206. */
  7207. TimeAxis.prototype._repaintMajorText = function (x, text) {
  7208. // reuse redundant label
  7209. var label = this.dom.redundant.majorTexts.shift();
  7210. if (!label) {
  7211. // create label
  7212. var content = document.createTextNode(text);
  7213. label = document.createElement('div');
  7214. label.className = 'text major';
  7215. label.appendChild(content);
  7216. this.frame.appendChild(label);
  7217. }
  7218. this.dom.majorTexts.push(label);
  7219. label.childNodes[0].nodeValue = text;
  7220. label.style.top = this.props.majorLabelTop + 'px';
  7221. label.style.left = x + 'px';
  7222. //label.title = title; // TODO: this is a heavy operation
  7223. };
  7224. /**
  7225. * Create a minor line for the axis at position x
  7226. * @param {Number} x
  7227. * @private
  7228. */
  7229. TimeAxis.prototype._repaintMinorLine = function (x) {
  7230. // reuse redundant line
  7231. var line = this.dom.redundant.minorLines.shift();
  7232. if (!line) {
  7233. // create vertical line
  7234. line = document.createElement('div');
  7235. line.className = 'grid vertical minor';
  7236. this.frame.appendChild(line);
  7237. }
  7238. this.dom.minorLines.push(line);
  7239. var props = this.props;
  7240. line.style.top = props.minorLineTop + 'px';
  7241. line.style.height = props.minorLineHeight + 'px';
  7242. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  7243. };
  7244. /**
  7245. * Create a Major line for the axis at position x
  7246. * @param {Number} x
  7247. * @private
  7248. */
  7249. TimeAxis.prototype._repaintMajorLine = function (x) {
  7250. // reuse redundant line
  7251. var line = this.dom.redundant.majorLines.shift();
  7252. if (!line) {
  7253. // create vertical line
  7254. line = document.createElement('DIV');
  7255. line.className = 'grid vertical major';
  7256. this.frame.appendChild(line);
  7257. }
  7258. this.dom.majorLines.push(line);
  7259. var props = this.props;
  7260. line.style.top = props.majorLineTop + 'px';
  7261. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  7262. line.style.height = props.majorLineHeight + 'px';
  7263. };
  7264. /**
  7265. * Repaint the horizontal line for the axis
  7266. * @private
  7267. */
  7268. TimeAxis.prototype._repaintLine = function() {
  7269. var line = this.dom.line,
  7270. frame = this.frame,
  7271. options = this.options;
  7272. // line before all axis elements
  7273. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  7274. if (line) {
  7275. // put this line at the end of all childs
  7276. frame.removeChild(line);
  7277. frame.appendChild(line);
  7278. }
  7279. else {
  7280. // create the axis line
  7281. line = document.createElement('div');
  7282. line.className = 'grid horizontal major';
  7283. frame.appendChild(line);
  7284. this.dom.line = line;
  7285. }
  7286. line.style.top = this.props.lineTop + 'px';
  7287. }
  7288. else {
  7289. if (line && axis.parentElement) {
  7290. frame.removeChild(axis.line);
  7291. delete this.dom.line;
  7292. }
  7293. }
  7294. };
  7295. /**
  7296. * Create characters used to determine the size of text on the axis
  7297. * @private
  7298. */
  7299. TimeAxis.prototype._repaintMeasureChars = function () {
  7300. // calculate the width and height of a single character
  7301. // this is used to calculate the step size, and also the positioning of the
  7302. // axis
  7303. var dom = this.dom,
  7304. text;
  7305. if (!dom.measureCharMinor) {
  7306. text = document.createTextNode('0');
  7307. var measureCharMinor = document.createElement('DIV');
  7308. measureCharMinor.className = 'text minor measure';
  7309. measureCharMinor.appendChild(text);
  7310. this.frame.appendChild(measureCharMinor);
  7311. dom.measureCharMinor = measureCharMinor;
  7312. }
  7313. if (!dom.measureCharMajor) {
  7314. text = document.createTextNode('0');
  7315. var measureCharMajor = document.createElement('DIV');
  7316. measureCharMajor.className = 'text major measure';
  7317. measureCharMajor.appendChild(text);
  7318. this.frame.appendChild(measureCharMajor);
  7319. dom.measureCharMajor = measureCharMajor;
  7320. }
  7321. };
  7322. /**
  7323. * Reflow the component
  7324. * @return {Boolean} resized
  7325. */
  7326. TimeAxis.prototype.reflow = function () {
  7327. var changed = 0,
  7328. update = util.updateProperty,
  7329. frame = this.frame,
  7330. range = this.range;
  7331. if (!range) {
  7332. throw new Error('Cannot repaint time axis: no range configured');
  7333. }
  7334. if (frame) {
  7335. changed += update(this, 'top', frame.offsetTop);
  7336. changed += update(this, 'left', frame.offsetLeft);
  7337. // calculate size of a character
  7338. var props = this.props,
  7339. showMinorLabels = this.getOption('showMinorLabels'),
  7340. showMajorLabels = this.getOption('showMajorLabels'),
  7341. measureCharMinor = this.dom.measureCharMinor,
  7342. measureCharMajor = this.dom.measureCharMajor;
  7343. if (measureCharMinor) {
  7344. props.minorCharHeight = measureCharMinor.clientHeight;
  7345. props.minorCharWidth = measureCharMinor.clientWidth;
  7346. }
  7347. if (measureCharMajor) {
  7348. props.majorCharHeight = measureCharMajor.clientHeight;
  7349. props.majorCharWidth = measureCharMajor.clientWidth;
  7350. }
  7351. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  7352. if (parentHeight != props.parentHeight) {
  7353. props.parentHeight = parentHeight;
  7354. changed += 1;
  7355. }
  7356. switch (this.getOption('orientation')) {
  7357. case 'bottom':
  7358. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  7359. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  7360. props.minorLabelTop = 0;
  7361. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  7362. props.minorLineTop = -this.top;
  7363. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  7364. props.minorLineWidth = 1; // TODO: really calculate width
  7365. props.majorLineTop = -this.top;
  7366. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  7367. props.majorLineWidth = 1; // TODO: really calculate width
  7368. props.lineTop = 0;
  7369. break;
  7370. case 'top':
  7371. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  7372. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  7373. props.majorLabelTop = 0;
  7374. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  7375. props.minorLineTop = props.minorLabelTop;
  7376. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  7377. props.minorLineWidth = 1; // TODO: really calculate width
  7378. props.majorLineTop = 0;
  7379. props.majorLineHeight = Math.max(parentHeight - this.top);
  7380. props.majorLineWidth = 1; // TODO: really calculate width
  7381. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  7382. break;
  7383. default:
  7384. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  7385. }
  7386. var height = props.minorLabelHeight + props.majorLabelHeight;
  7387. changed += update(this, 'width', frame.offsetWidth);
  7388. changed += update(this, 'height', height);
  7389. // calculate range and step
  7390. this._updateConversion();
  7391. var start = util.convert(range.start, 'Number'),
  7392. end = util.convert(range.end, 'Number'),
  7393. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  7394. -this.toTime(0).valueOf();
  7395. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  7396. changed += update(props.range, 'start', start);
  7397. changed += update(props.range, 'end', end);
  7398. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  7399. }
  7400. return (changed > 0);
  7401. };
  7402. /**
  7403. * Calculate the factor and offset to convert a position on screen to the
  7404. * corresponding date and vice versa.
  7405. * After the method _updateConversion is executed once, the methods toTime
  7406. * and toScreen can be used.
  7407. * @private
  7408. */
  7409. TimeAxis.prototype._updateConversion = function() {
  7410. var range = this.range;
  7411. if (!range) {
  7412. throw new Error('No range configured');
  7413. }
  7414. if (range.conversion) {
  7415. this.conversion = range.conversion(this.width);
  7416. }
  7417. else {
  7418. this.conversion = Range.conversion(range.start, range.end, this.width);
  7419. }
  7420. };
  7421. /**
  7422. * A current time bar
  7423. * @param {Component} parent
  7424. * @param {Component[]} [depends] Components on which this components depends
  7425. * (except for the parent)
  7426. * @param {Object} [options] Available parameters:
  7427. * {Boolean} [showCurrentTime]
  7428. * @constructor CurrentTime
  7429. * @extends Component
  7430. */
  7431. function CurrentTime (parent, depends, options) {
  7432. this.id = util.randomUUID();
  7433. this.parent = parent;
  7434. this.depends = depends;
  7435. this.options = options || {};
  7436. this.defaultOptions = {
  7437. showCurrentTime: false
  7438. };
  7439. }
  7440. CurrentTime.prototype = new Component();
  7441. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  7442. /**
  7443. * Get the container element of the bar, which can be used by a child to
  7444. * add its own widgets.
  7445. * @returns {HTMLElement} container
  7446. */
  7447. CurrentTime.prototype.getContainer = function () {
  7448. return this.frame;
  7449. };
  7450. /**
  7451. * Repaint the component
  7452. * @return {Boolean} changed
  7453. */
  7454. CurrentTime.prototype.repaint = function () {
  7455. var bar = this.frame,
  7456. parent = this.parent,
  7457. parentContainer = parent.parent.getContainer();
  7458. if (!parent) {
  7459. throw new Error('Cannot repaint bar: no parent attached');
  7460. }
  7461. if (!parentContainer) {
  7462. throw new Error('Cannot repaint bar: parent has no container element');
  7463. }
  7464. if (!this.getOption('showCurrentTime')) {
  7465. if (bar) {
  7466. parentContainer.removeChild(bar);
  7467. delete this.frame;
  7468. }
  7469. return;
  7470. }
  7471. if (!bar) {
  7472. bar = document.createElement('div');
  7473. bar.className = 'currenttime';
  7474. bar.style.position = 'absolute';
  7475. bar.style.top = '0px';
  7476. bar.style.height = '100%';
  7477. parentContainer.appendChild(bar);
  7478. this.frame = bar;
  7479. }
  7480. if (!parent.conversion) {
  7481. parent._updateConversion();
  7482. }
  7483. var now = new Date();
  7484. var x = parent.toScreen(now);
  7485. bar.style.left = x + 'px';
  7486. bar.title = 'Current time: ' + now;
  7487. // start a timer to adjust for the new time
  7488. if (this.currentTimeTimer !== undefined) {
  7489. clearTimeout(this.currentTimeTimer);
  7490. delete this.currentTimeTimer;
  7491. }
  7492. var timeline = this;
  7493. var interval = 1 / parent.conversion.factor / 2;
  7494. if (interval < 30) {
  7495. interval = 30;
  7496. }
  7497. this.currentTimeTimer = setTimeout(function() {
  7498. timeline.repaint();
  7499. }, interval);
  7500. return false;
  7501. };
  7502. /**
  7503. * A custom time bar
  7504. * @param {Component} parent
  7505. * @param {Component[]} [depends] Components on which this components depends
  7506. * (except for the parent)
  7507. * @param {Object} [options] Available parameters:
  7508. * {Boolean} [showCustomTime]
  7509. * @constructor CustomTime
  7510. * @extends Component
  7511. */
  7512. function CustomTime (parent, depends, options) {
  7513. this.id = util.randomUUID();
  7514. this.parent = parent;
  7515. this.depends = depends;
  7516. this.options = options || {};
  7517. this.defaultOptions = {
  7518. showCustomTime: false
  7519. };
  7520. this.listeners = [];
  7521. this.customTime = new Date();
  7522. }
  7523. CustomTime.prototype = new Component();
  7524. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  7525. /**
  7526. * Get the container element of the bar, which can be used by a child to
  7527. * add its own widgets.
  7528. * @returns {HTMLElement} container
  7529. */
  7530. CustomTime.prototype.getContainer = function () {
  7531. return this.frame;
  7532. };
  7533. /**
  7534. * Repaint the component
  7535. * @return {Boolean} changed
  7536. */
  7537. CustomTime.prototype.repaint = function () {
  7538. var bar = this.frame,
  7539. parent = this.parent,
  7540. parentContainer = parent.parent.getContainer();
  7541. if (!parent) {
  7542. throw new Error('Cannot repaint bar: no parent attached');
  7543. }
  7544. if (!parentContainer) {
  7545. throw new Error('Cannot repaint bar: parent has no container element');
  7546. }
  7547. if (!this.getOption('showCustomTime')) {
  7548. if (bar) {
  7549. parentContainer.removeChild(bar);
  7550. delete this.frame;
  7551. }
  7552. return;
  7553. }
  7554. if (!bar) {
  7555. bar = document.createElement('div');
  7556. bar.className = 'customtime';
  7557. bar.style.position = 'absolute';
  7558. bar.style.top = '0px';
  7559. bar.style.height = '100%';
  7560. parentContainer.appendChild(bar);
  7561. var drag = document.createElement('div');
  7562. drag.style.position = 'relative';
  7563. drag.style.top = '0px';
  7564. drag.style.left = '-10px';
  7565. drag.style.height = '100%';
  7566. drag.style.width = '20px';
  7567. bar.appendChild(drag);
  7568. this.frame = bar;
  7569. this.subscribe(this, 'movetime');
  7570. }
  7571. if (!parent.conversion) {
  7572. parent._updateConversion();
  7573. }
  7574. var x = parent.toScreen(this.customTime);
  7575. bar.style.left = x + 'px';
  7576. bar.title = 'Time: ' + this.customTime;
  7577. return false;
  7578. };
  7579. /**
  7580. * Set custom time.
  7581. * @param {Date} time
  7582. */
  7583. CustomTime.prototype._setCustomTime = function(time) {
  7584. this.customTime = new Date(time.valueOf());
  7585. this.repaint();
  7586. };
  7587. /**
  7588. * Retrieve the current custom time.
  7589. * @return {Date} customTime
  7590. */
  7591. CustomTime.prototype._getCustomTime = function() {
  7592. return new Date(this.customTime.valueOf());
  7593. };
  7594. /**
  7595. * Add listeners for mouse and touch events to the component
  7596. * @param {Component} component
  7597. */
  7598. CustomTime.prototype.subscribe = function (component, event) {
  7599. var me = this;
  7600. var listener = {
  7601. component: component,
  7602. event: event,
  7603. callback: function (event) {
  7604. me._onMouseDown(event, listener);
  7605. },
  7606. params: {}
  7607. };
  7608. component.on('mousedown', listener.callback);
  7609. me.listeners.push(listener);
  7610. };
  7611. /**
  7612. * Event handler
  7613. * @param {String} event name of the event, for example 'click', 'mousemove'
  7614. * @param {function} callback callback handler, invoked with the raw HTML Event
  7615. * as parameter.
  7616. */
  7617. CustomTime.prototype.on = function (event, callback) {
  7618. var bar = this.frame;
  7619. if (!bar) {
  7620. throw new Error('Cannot add event listener: no parent attached');
  7621. }
  7622. events.addListener(this, event, callback);
  7623. util.addEventListener(bar, event, callback);
  7624. };
  7625. /**
  7626. * Start moving horizontally
  7627. * @param {Event} event
  7628. * @param {Object} listener Listener containing the component and params
  7629. * @private
  7630. */
  7631. CustomTime.prototype._onMouseDown = function(event, listener) {
  7632. event = event || window.event;
  7633. var params = listener.params;
  7634. // only react on left mouse button down
  7635. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  7636. if (!leftButtonDown) {
  7637. return;
  7638. }
  7639. // get mouse position
  7640. params.mouseX = util.getPageX(event);
  7641. params.moved = false;
  7642. params.customTime = this.customTime;
  7643. // add event listeners to handle moving the custom time bar
  7644. var me = this;
  7645. if (!params.onMouseMove) {
  7646. params.onMouseMove = function (event) {
  7647. me._onMouseMove(event, listener);
  7648. };
  7649. util.addEventListener(document, 'mousemove', params.onMouseMove);
  7650. }
  7651. if (!params.onMouseUp) {
  7652. params.onMouseUp = function (event) {
  7653. me._onMouseUp(event, listener);
  7654. };
  7655. util.addEventListener(document, 'mouseup', params.onMouseUp);
  7656. }
  7657. util.stopPropagation(event);
  7658. util.preventDefault(event);
  7659. };
  7660. /**
  7661. * Perform moving operating.
  7662. * This function activated from within the funcion CustomTime._onMouseDown().
  7663. * @param {Event} event
  7664. * @param {Object} listener
  7665. * @private
  7666. */
  7667. CustomTime.prototype._onMouseMove = function (event, listener) {
  7668. event = event || window.event;
  7669. var params = listener.params;
  7670. var parent = this.parent;
  7671. // calculate change in mouse position
  7672. var mouseX = util.getPageX(event);
  7673. if (params.mouseX === undefined) {
  7674. params.mouseX = mouseX;
  7675. }
  7676. var diff = mouseX - params.mouseX;
  7677. // if mouse movement is big enough, register it as a "moved" event
  7678. if (Math.abs(diff) >= 1) {
  7679. params.moved = true;
  7680. }
  7681. var x = parent.toScreen(params.customTime);
  7682. var xnew = x + diff;
  7683. var time = parent.toTime(xnew);
  7684. this._setCustomTime(time);
  7685. // fire a timechange event
  7686. events.trigger(this, 'timechange', {customTime: this.customTime});
  7687. util.preventDefault(event);
  7688. };
  7689. /**
  7690. * Stop moving operating.
  7691. * This function activated from within the function CustomTime._onMouseDown().
  7692. * @param {event} event
  7693. * @param {Object} listener
  7694. * @private
  7695. */
  7696. CustomTime.prototype._onMouseUp = function (event, listener) {
  7697. event = event || window.event;
  7698. var params = listener.params;
  7699. // remove event listeners here, important for Safari
  7700. if (params.onMouseMove) {
  7701. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  7702. params.onMouseMove = null;
  7703. }
  7704. if (params.onMouseUp) {
  7705. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  7706. params.onMouseUp = null;
  7707. }
  7708. if (params.moved) {
  7709. // fire a timechanged event
  7710. events.trigger(this, 'timechanged', {customTime: this.customTime});
  7711. }
  7712. };
  7713. /**
  7714. * An ItemSet holds a set of items and ranges which can be displayed in a
  7715. * range. The width is determined by the parent of the ItemSet, and the height
  7716. * is determined by the size of the items.
  7717. * @param {Component} parent
  7718. * @param {Component[]} [depends] Components on which this components depends
  7719. * (except for the parent)
  7720. * @param {Object} [options] See ItemSet.setOptions for the available
  7721. * options.
  7722. * @constructor ItemSet
  7723. * @extends Panel
  7724. */
  7725. // TODO: improve performance by replacing all Array.forEach with a for loop
  7726. function ItemSet(parent, depends, options) {
  7727. this.id = util.randomUUID();
  7728. this.parent = parent;
  7729. this.depends = depends;
  7730. // one options object is shared by this itemset and all its items
  7731. this.options = options || {};
  7732. this.defaultOptions = {
  7733. type: 'box',
  7734. align: 'center',
  7735. orientation: 'bottom',
  7736. margin: {
  7737. axis: 20,
  7738. item: 10
  7739. },
  7740. padding: 5
  7741. };
  7742. this.dom = {};
  7743. var me = this;
  7744. this.itemsData = null; // DataSet
  7745. this.range = null; // Range or Object {start: number, end: number}
  7746. this.listeners = {
  7747. 'add': function (event, params, senderId) {
  7748. if (senderId != me.id) {
  7749. me._onAdd(params.items);
  7750. }
  7751. },
  7752. 'update': function (event, params, senderId) {
  7753. if (senderId != me.id) {
  7754. me._onUpdate(params.items);
  7755. }
  7756. },
  7757. 'remove': function (event, params, senderId) {
  7758. if (senderId != me.id) {
  7759. me._onRemove(params.items);
  7760. }
  7761. }
  7762. };
  7763. this.items = {}; // object with an Item for every data item
  7764. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  7765. this.stack = new Stack(this, Object.create(this.options));
  7766. this.conversion = null;
  7767. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  7768. }
  7769. ItemSet.prototype = new Panel();
  7770. // available item types will be registered here
  7771. ItemSet.types = {
  7772. box: ItemBox,
  7773. range: ItemRange,
  7774. rangeoverflow: ItemRangeOverflow,
  7775. point: ItemPoint
  7776. };
  7777. /**
  7778. * Set options for the ItemSet. Existing options will be extended/overwritten.
  7779. * @param {Object} [options] The following options are available:
  7780. * {String | function} [className]
  7781. * class name for the itemset
  7782. * {String} [type]
  7783. * Default type for the items. Choose from 'box'
  7784. * (default), 'point', or 'range'. The default
  7785. * Style can be overwritten by individual items.
  7786. * {String} align
  7787. * Alignment for the items, only applicable for
  7788. * ItemBox. Choose 'center' (default), 'left', or
  7789. * 'right'.
  7790. * {String} orientation
  7791. * Orientation of the item set. Choose 'top' or
  7792. * 'bottom' (default).
  7793. * {Number} margin.axis
  7794. * Margin between the axis and the items in pixels.
  7795. * Default is 20.
  7796. * {Number} margin.item
  7797. * Margin between items in pixels. Default is 10.
  7798. * {Number} padding
  7799. * Padding of the contents of an item in pixels.
  7800. * Must correspond with the items css. Default is 5.
  7801. */
  7802. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  7803. /**
  7804. * Set range (start and end).
  7805. * @param {Range | Object} range A Range or an object containing start and end.
  7806. */
  7807. ItemSet.prototype.setRange = function setRange(range) {
  7808. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  7809. throw new TypeError('Range must be an instance of Range, ' +
  7810. 'or an object containing start and end.');
  7811. }
  7812. this.range = range;
  7813. };
  7814. /**
  7815. * Repaint the component
  7816. * @return {Boolean} changed
  7817. */
  7818. ItemSet.prototype.repaint = function repaint() {
  7819. var changed = 0,
  7820. update = util.updateProperty,
  7821. asSize = util.option.asSize,
  7822. options = this.options,
  7823. orientation = this.getOption('orientation'),
  7824. defaultOptions = this.defaultOptions,
  7825. frame = this.frame;
  7826. if (!frame) {
  7827. frame = document.createElement('div');
  7828. frame.className = 'itemset';
  7829. var className = options.className;
  7830. if (className) {
  7831. util.addClassName(frame, util.option.asString(className));
  7832. }
  7833. // create background panel
  7834. var background = document.createElement('div');
  7835. background.className = 'background';
  7836. frame.appendChild(background);
  7837. this.dom.background = background;
  7838. // create foreground panel
  7839. var foreground = document.createElement('div');
  7840. foreground.className = 'foreground';
  7841. frame.appendChild(foreground);
  7842. this.dom.foreground = foreground;
  7843. // create axis panel
  7844. var axis = document.createElement('div');
  7845. axis.className = 'itemset-axis';
  7846. //frame.appendChild(axis);
  7847. this.dom.axis = axis;
  7848. this.frame = frame;
  7849. changed += 1;
  7850. }
  7851. if (!this.parent) {
  7852. throw new Error('Cannot repaint itemset: no parent attached');
  7853. }
  7854. var parentContainer = this.parent.getContainer();
  7855. if (!parentContainer) {
  7856. throw new Error('Cannot repaint itemset: parent has no container element');
  7857. }
  7858. if (!frame.parentNode) {
  7859. parentContainer.appendChild(frame);
  7860. changed += 1;
  7861. }
  7862. if (!this.dom.axis.parentNode) {
  7863. parentContainer.appendChild(this.dom.axis);
  7864. changed += 1;
  7865. }
  7866. // reposition frame
  7867. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7868. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  7869. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7870. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  7871. // reposition axis
  7872. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  7873. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  7874. if (orientation == 'bottom') {
  7875. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  7876. }
  7877. else { // orientation == 'top'
  7878. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  7879. }
  7880. this._updateConversion();
  7881. var me = this,
  7882. queue = this.queue,
  7883. itemsData = this.itemsData,
  7884. items = this.items,
  7885. dataOptions = {
  7886. // TODO: cleanup
  7887. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  7888. };
  7889. // show/hide added/changed/removed items
  7890. Object.keys(queue).forEach(function (id) {
  7891. //var entry = queue[id];
  7892. var action = queue[id];
  7893. var item = items[id];
  7894. //var item = entry.item;
  7895. //noinspection FallthroughInSwitchStatementJS
  7896. switch (action) {
  7897. case 'add':
  7898. case 'update':
  7899. var itemData = itemsData && itemsData.get(id, dataOptions);
  7900. if (itemData) {
  7901. var type = itemData.type ||
  7902. (itemData.start && itemData.end && 'range') ||
  7903. options.type ||
  7904. 'box';
  7905. var constructor = ItemSet.types[type];
  7906. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  7907. if (item) {
  7908. // update item
  7909. if (!constructor || !(item instanceof constructor)) {
  7910. // item type has changed, hide and delete the item
  7911. changed += item.hide();
  7912. item = null;
  7913. }
  7914. else {
  7915. item.data = itemData; // TODO: create a method item.setData ?
  7916. changed++;
  7917. }
  7918. }
  7919. if (!item) {
  7920. // create item
  7921. if (constructor) {
  7922. item = new constructor(me, itemData, options, defaultOptions);
  7923. changed++;
  7924. }
  7925. else {
  7926. throw new TypeError('Unknown item type "' + type + '"');
  7927. }
  7928. }
  7929. // force a repaint (not only a reposition)
  7930. item.repaint();
  7931. items[id] = item;
  7932. }
  7933. // update queue
  7934. delete queue[id];
  7935. break;
  7936. case 'remove':
  7937. if (item) {
  7938. // remove DOM of the item
  7939. changed += item.hide();
  7940. }
  7941. // update lists
  7942. delete items[id];
  7943. delete queue[id];
  7944. break;
  7945. default:
  7946. console.log('Error: unknown action "' + action + '"');
  7947. }
  7948. });
  7949. // reposition all items. Show items only when in the visible area
  7950. util.forEach(this.items, function (item) {
  7951. if (item.visible) {
  7952. changed += item.show();
  7953. item.reposition();
  7954. }
  7955. else {
  7956. changed += item.hide();
  7957. }
  7958. });
  7959. return (changed > 0);
  7960. };
  7961. /**
  7962. * Get the foreground container element
  7963. * @return {HTMLElement} foreground
  7964. */
  7965. ItemSet.prototype.getForeground = function getForeground() {
  7966. return this.dom.foreground;
  7967. };
  7968. /**
  7969. * Get the background container element
  7970. * @return {HTMLElement} background
  7971. */
  7972. ItemSet.prototype.getBackground = function getBackground() {
  7973. return this.dom.background;
  7974. };
  7975. /**
  7976. * Get the axis container element
  7977. * @return {HTMLElement} axis
  7978. */
  7979. ItemSet.prototype.getAxis = function getAxis() {
  7980. return this.dom.axis;
  7981. };
  7982. /**
  7983. * Reflow the component
  7984. * @return {Boolean} resized
  7985. */
  7986. ItemSet.prototype.reflow = function reflow () {
  7987. var changed = 0,
  7988. options = this.options,
  7989. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  7990. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  7991. update = util.updateProperty,
  7992. asNumber = util.option.asNumber,
  7993. asSize = util.option.asSize,
  7994. frame = this.frame;
  7995. if (frame) {
  7996. this._updateConversion();
  7997. util.forEach(this.items, function (item) {
  7998. changed += item.reflow();
  7999. });
  8000. // TODO: stack.update should be triggered via an event, in stack itself
  8001. // TODO: only update the stack when there are changed items
  8002. this.stack.update();
  8003. var maxHeight = asNumber(options.maxHeight);
  8004. var fixedHeight = (asSize(options.height) != null);
  8005. var height;
  8006. if (fixedHeight) {
  8007. height = frame.offsetHeight;
  8008. }
  8009. else {
  8010. // height is not specified, determine the height from the height and positioned items
  8011. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  8012. if (visibleItems.length) {
  8013. var min = visibleItems[0].top;
  8014. var max = visibleItems[0].top + visibleItems[0].height;
  8015. util.forEach(visibleItems, function (item) {
  8016. min = Math.min(min, item.top);
  8017. max = Math.max(max, (item.top + item.height));
  8018. });
  8019. height = (max - min) + marginAxis + marginItem;
  8020. }
  8021. else {
  8022. height = marginAxis + marginItem;
  8023. }
  8024. }
  8025. if (maxHeight != null) {
  8026. height = Math.min(height, maxHeight);
  8027. }
  8028. changed += update(this, 'height', height);
  8029. // calculate height from items
  8030. changed += update(this, 'top', frame.offsetTop);
  8031. changed += update(this, 'left', frame.offsetLeft);
  8032. changed += update(this, 'width', frame.offsetWidth);
  8033. }
  8034. else {
  8035. changed += 1;
  8036. }
  8037. return (changed > 0);
  8038. };
  8039. /**
  8040. * Hide this component from the DOM
  8041. * @return {Boolean} changed
  8042. */
  8043. ItemSet.prototype.hide = function hide() {
  8044. var changed = false;
  8045. // remove the DOM
  8046. if (this.frame && this.frame.parentNode) {
  8047. this.frame.parentNode.removeChild(this.frame);
  8048. changed = true;
  8049. }
  8050. if (this.dom.axis && this.dom.axis.parentNode) {
  8051. this.dom.axis.parentNode.removeChild(this.dom.axis);
  8052. changed = true;
  8053. }
  8054. return changed;
  8055. };
  8056. /**
  8057. * Set items
  8058. * @param {vis.DataSet | null} items
  8059. */
  8060. ItemSet.prototype.setItems = function setItems(items) {
  8061. var me = this,
  8062. ids,
  8063. oldItemsData = this.itemsData;
  8064. // replace the dataset
  8065. if (!items) {
  8066. this.itemsData = null;
  8067. }
  8068. else if (items instanceof DataSet || items instanceof DataView) {
  8069. this.itemsData = items;
  8070. }
  8071. else {
  8072. throw new TypeError('Data must be an instance of DataSet');
  8073. }
  8074. if (oldItemsData) {
  8075. // unsubscribe from old dataset
  8076. util.forEach(this.listeners, function (callback, event) {
  8077. oldItemsData.unsubscribe(event, callback);
  8078. });
  8079. // remove all drawn items
  8080. ids = oldItemsData.getIds();
  8081. this._onRemove(ids);
  8082. }
  8083. if (this.itemsData) {
  8084. // subscribe to new dataset
  8085. var id = this.id;
  8086. util.forEach(this.listeners, function (callback, event) {
  8087. me.itemsData.subscribe(event, callback, id);
  8088. });
  8089. // draw all new items
  8090. ids = this.itemsData.getIds();
  8091. this._onAdd(ids);
  8092. }
  8093. };
  8094. /**
  8095. * Get the current items items
  8096. * @returns {vis.DataSet | null}
  8097. */
  8098. ItemSet.prototype.getItems = function getItems() {
  8099. return this.itemsData;
  8100. };
  8101. /**
  8102. * Handle updated items
  8103. * @param {Number[]} ids
  8104. * @private
  8105. */
  8106. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  8107. this._toQueue('update', ids);
  8108. };
  8109. /**
  8110. * Handle changed items
  8111. * @param {Number[]} ids
  8112. * @private
  8113. */
  8114. ItemSet.prototype._onAdd = function _onAdd(ids) {
  8115. this._toQueue('add', ids);
  8116. };
  8117. /**
  8118. * Handle removed items
  8119. * @param {Number[]} ids
  8120. * @private
  8121. */
  8122. ItemSet.prototype._onRemove = function _onRemove(ids) {
  8123. this._toQueue('remove', ids);
  8124. };
  8125. /**
  8126. * Put items in the queue to be added/updated/remove
  8127. * @param {String} action can be 'add', 'update', 'remove'
  8128. * @param {Number[]} ids
  8129. */
  8130. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  8131. var queue = this.queue;
  8132. ids.forEach(function (id) {
  8133. queue[id] = action;
  8134. });
  8135. if (this.controller) {
  8136. //this.requestReflow();
  8137. this.requestRepaint();
  8138. }
  8139. };
  8140. /**
  8141. * Calculate the factor and offset to convert a position on screen to the
  8142. * corresponding date and vice versa.
  8143. * After the method _updateConversion is executed once, the methods toTime
  8144. * and toScreen can be used.
  8145. * @private
  8146. */
  8147. ItemSet.prototype._updateConversion = function _updateConversion() {
  8148. var range = this.range;
  8149. if (!range) {
  8150. throw new Error('No range configured');
  8151. }
  8152. if (range.conversion) {
  8153. this.conversion = range.conversion(this.width);
  8154. }
  8155. else {
  8156. this.conversion = Range.conversion(range.start, range.end, this.width);
  8157. }
  8158. };
  8159. /**
  8160. * Convert a position on screen (pixels) to a datetime
  8161. * Before this method can be used, the method _updateConversion must be
  8162. * executed once.
  8163. * @param {int} x Position on the screen in pixels
  8164. * @return {Date} time The datetime the corresponds with given position x
  8165. */
  8166. ItemSet.prototype.toTime = function toTime(x) {
  8167. var conversion = this.conversion;
  8168. return new Date(x / conversion.factor + conversion.offset);
  8169. };
  8170. /**
  8171. * Convert a datetime (Date object) into a position on the screen
  8172. * Before this method can be used, the method _updateConversion must be
  8173. * executed once.
  8174. * @param {Date} time A date
  8175. * @return {int} x The position on the screen in pixels which corresponds
  8176. * with the given date.
  8177. */
  8178. ItemSet.prototype.toScreen = function toScreen(time) {
  8179. var conversion = this.conversion;
  8180. return (time.valueOf() - conversion.offset) * conversion.factor;
  8181. };
  8182. /**
  8183. * @constructor Item
  8184. * @param {ItemSet} parent
  8185. * @param {Object} data Object containing (optional) parameters type,
  8186. * start, end, content, group, className.
  8187. * @param {Object} [options] Options to set initial property values
  8188. * @param {Object} [defaultOptions] default options
  8189. * // TODO: describe available options
  8190. */
  8191. function Item (parent, data, options, defaultOptions) {
  8192. this.parent = parent;
  8193. this.data = data;
  8194. this.dom = null;
  8195. this.options = options || {};
  8196. this.defaultOptions = defaultOptions || {};
  8197. this.selected = false;
  8198. this.visible = false;
  8199. this.top = 0;
  8200. this.left = 0;
  8201. this.width = 0;
  8202. this.height = 0;
  8203. }
  8204. /**
  8205. * Select current item
  8206. */
  8207. Item.prototype.select = function select() {
  8208. this.selected = true;
  8209. };
  8210. /**
  8211. * Unselect current item
  8212. */
  8213. Item.prototype.unselect = function unselect() {
  8214. this.selected = false;
  8215. };
  8216. /**
  8217. * Show the Item in the DOM (when not already visible)
  8218. * @return {Boolean} changed
  8219. */
  8220. Item.prototype.show = function show() {
  8221. return false;
  8222. };
  8223. /**
  8224. * Hide the Item from the DOM (when visible)
  8225. * @return {Boolean} changed
  8226. */
  8227. Item.prototype.hide = function hide() {
  8228. return false;
  8229. };
  8230. /**
  8231. * Repaint the item
  8232. * @return {Boolean} changed
  8233. */
  8234. Item.prototype.repaint = function repaint() {
  8235. // should be implemented by the item
  8236. return false;
  8237. };
  8238. /**
  8239. * Reflow the item
  8240. * @return {Boolean} resized
  8241. */
  8242. Item.prototype.reflow = function reflow() {
  8243. // should be implemented by the item
  8244. return false;
  8245. };
  8246. /**
  8247. * Return the items width
  8248. * @return {Integer} width
  8249. */
  8250. Item.prototype.getWidth = function getWidth() {
  8251. return this.width;
  8252. }
  8253. /**
  8254. * @constructor ItemBox
  8255. * @extends Item
  8256. * @param {ItemSet} parent
  8257. * @param {Object} data Object containing parameters start
  8258. * content, className.
  8259. * @param {Object} [options] Options to set initial property values
  8260. * @param {Object} [defaultOptions] default options
  8261. * // TODO: describe available options
  8262. */
  8263. function ItemBox (parent, data, options, defaultOptions) {
  8264. this.props = {
  8265. dot: {
  8266. left: 0,
  8267. top: 0,
  8268. width: 0,
  8269. height: 0
  8270. },
  8271. line: {
  8272. top: 0,
  8273. left: 0,
  8274. width: 0,
  8275. height: 0
  8276. }
  8277. };
  8278. Item.call(this, parent, data, options, defaultOptions);
  8279. }
  8280. ItemBox.prototype = new Item (null, null);
  8281. /**
  8282. * Select the item
  8283. * @override
  8284. */
  8285. ItemBox.prototype.select = function select() {
  8286. this.selected = true;
  8287. // TODO: select and unselect
  8288. };
  8289. /**
  8290. * Unselect the item
  8291. * @override
  8292. */
  8293. ItemBox.prototype.unselect = function unselect() {
  8294. this.selected = false;
  8295. // TODO: select and unselect
  8296. };
  8297. /**
  8298. * Repaint the item
  8299. * @return {Boolean} changed
  8300. */
  8301. ItemBox.prototype.repaint = function repaint() {
  8302. // TODO: make an efficient repaint
  8303. var changed = false;
  8304. var dom = this.dom;
  8305. if (!dom) {
  8306. this._create();
  8307. dom = this.dom;
  8308. changed = true;
  8309. }
  8310. if (dom) {
  8311. if (!this.parent) {
  8312. throw new Error('Cannot repaint item: no parent attached');
  8313. }
  8314. var foreground = this.parent.getForeground();
  8315. if (!foreground) {
  8316. throw new Error('Cannot repaint time axis: ' +
  8317. 'parent has no foreground container element');
  8318. }
  8319. var background = this.parent.getBackground();
  8320. if (!background) {
  8321. throw new Error('Cannot repaint time axis: ' +
  8322. 'parent has no background container element');
  8323. }
  8324. var axis = this.parent.getAxis();
  8325. if (!background) {
  8326. throw new Error('Cannot repaint time axis: ' +
  8327. 'parent has no axis container element');
  8328. }
  8329. if (!dom.box.parentNode) {
  8330. foreground.appendChild(dom.box);
  8331. changed = true;
  8332. }
  8333. if (!dom.line.parentNode) {
  8334. background.appendChild(dom.line);
  8335. changed = true;
  8336. }
  8337. if (!dom.dot.parentNode) {
  8338. axis.appendChild(dom.dot);
  8339. changed = true;
  8340. }
  8341. // update contents
  8342. if (this.data.content != this.content) {
  8343. this.content = this.data.content;
  8344. if (this.content instanceof Element) {
  8345. dom.content.innerHTML = '';
  8346. dom.content.appendChild(this.content);
  8347. }
  8348. else if (this.data.content != undefined) {
  8349. dom.content.innerHTML = this.content;
  8350. }
  8351. else {
  8352. throw new Error('Property "content" missing in item ' + this.data.id);
  8353. }
  8354. changed = true;
  8355. }
  8356. // update class
  8357. var className = (this.data.className? ' ' + this.data.className : '') +
  8358. (this.selected ? ' selected' : '');
  8359. if (this.className != className) {
  8360. this.className = className;
  8361. dom.box.className = 'item box' + className;
  8362. dom.line.className = 'item line' + className;
  8363. dom.dot.className = 'item dot' + className;
  8364. changed = true;
  8365. }
  8366. }
  8367. return changed;
  8368. };
  8369. /**
  8370. * Show the item in the DOM (when not already visible). The items DOM will
  8371. * be created when needed.
  8372. * @return {Boolean} changed
  8373. */
  8374. ItemBox.prototype.show = function show() {
  8375. if (!this.dom || !this.dom.box.parentNode) {
  8376. return this.repaint();
  8377. }
  8378. else {
  8379. return false;
  8380. }
  8381. };
  8382. /**
  8383. * Hide the item from the DOM (when visible)
  8384. * @return {Boolean} changed
  8385. */
  8386. ItemBox.prototype.hide = function hide() {
  8387. var changed = false,
  8388. dom = this.dom;
  8389. if (dom) {
  8390. if (dom.box.parentNode) {
  8391. dom.box.parentNode.removeChild(dom.box);
  8392. changed = true;
  8393. }
  8394. if (dom.line.parentNode) {
  8395. dom.line.parentNode.removeChild(dom.line);
  8396. }
  8397. if (dom.dot.parentNode) {
  8398. dom.dot.parentNode.removeChild(dom.dot);
  8399. }
  8400. }
  8401. return changed;
  8402. };
  8403. /**
  8404. * Reflow the item: calculate its actual size and position from the DOM
  8405. * @return {boolean} resized returns true if the axis is resized
  8406. * @override
  8407. */
  8408. ItemBox.prototype.reflow = function reflow() {
  8409. var changed = 0,
  8410. update,
  8411. dom,
  8412. props,
  8413. options,
  8414. margin,
  8415. start,
  8416. align,
  8417. orientation,
  8418. top,
  8419. left,
  8420. data,
  8421. range;
  8422. if (this.data.start == undefined) {
  8423. throw new Error('Property "start" missing in item ' + this.data.id);
  8424. }
  8425. data = this.data;
  8426. range = this.parent && this.parent.range;
  8427. if (data && range) {
  8428. // TODO: account for the width of the item
  8429. var interval = (range.end - range.start);
  8430. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  8431. }
  8432. else {
  8433. this.visible = false;
  8434. }
  8435. if (this.visible) {
  8436. dom = this.dom;
  8437. if (dom) {
  8438. update = util.updateProperty;
  8439. props = this.props;
  8440. options = this.options;
  8441. start = this.parent.toScreen(this.data.start);
  8442. align = options.align || this.defaultOptions.align;
  8443. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  8444. orientation = options.orientation || this.defaultOptions.orientation;
  8445. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  8446. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  8447. changed += update(props.line, 'width', dom.line.offsetWidth);
  8448. changed += update(props.line, 'height', dom.line.offsetHeight);
  8449. changed += update(props.line, 'top', dom.line.offsetTop);
  8450. changed += update(this, 'width', dom.box.offsetWidth);
  8451. changed += update(this, 'height', dom.box.offsetHeight);
  8452. if (align == 'right') {
  8453. left = start - this.width;
  8454. }
  8455. else if (align == 'left') {
  8456. left = start;
  8457. }
  8458. else {
  8459. // default or 'center'
  8460. left = start - this.width / 2;
  8461. }
  8462. changed += update(this, 'left', left);
  8463. changed += update(props.line, 'left', start - props.line.width / 2);
  8464. changed += update(props.dot, 'left', start - props.dot.width / 2);
  8465. changed += update(props.dot, 'top', -props.dot.height / 2);
  8466. if (orientation == 'top') {
  8467. top = margin;
  8468. changed += update(this, 'top', top);
  8469. }
  8470. else {
  8471. // default or 'bottom'
  8472. var parentHeight = this.parent.height;
  8473. top = parentHeight - this.height - margin;
  8474. changed += update(this, 'top', top);
  8475. }
  8476. }
  8477. else {
  8478. changed += 1;
  8479. }
  8480. }
  8481. return (changed > 0);
  8482. };
  8483. /**
  8484. * Create an items DOM
  8485. * @private
  8486. */
  8487. ItemBox.prototype._create = function _create() {
  8488. var dom = this.dom;
  8489. if (!dom) {
  8490. this.dom = dom = {};
  8491. // create the box
  8492. dom.box = document.createElement('DIV');
  8493. // className is updated in repaint()
  8494. // contents box (inside the background box). used for making margins
  8495. dom.content = document.createElement('DIV');
  8496. dom.content.className = 'content';
  8497. dom.box.appendChild(dom.content);
  8498. // line to axis
  8499. dom.line = document.createElement('DIV');
  8500. dom.line.className = 'line';
  8501. // dot on axis
  8502. dom.dot = document.createElement('DIV');
  8503. dom.dot.className = 'dot';
  8504. }
  8505. };
  8506. /**
  8507. * Reposition the item, recalculate its left, top, and width, using the current
  8508. * range and size of the items itemset
  8509. * @override
  8510. */
  8511. ItemBox.prototype.reposition = function reposition() {
  8512. var dom = this.dom,
  8513. props = this.props,
  8514. orientation = this.options.orientation || this.defaultOptions.orientation;
  8515. if (dom) {
  8516. var box = dom.box,
  8517. line = dom.line,
  8518. dot = dom.dot;
  8519. box.style.left = this.left + 'px';
  8520. box.style.top = this.top + 'px';
  8521. line.style.left = props.line.left + 'px';
  8522. if (orientation == 'top') {
  8523. line.style.top = 0 + 'px';
  8524. line.style.height = this.top + 'px';
  8525. }
  8526. else {
  8527. // orientation 'bottom'
  8528. line.style.top = (this.top + this.height) + 'px';
  8529. line.style.height = Math.max(this.parent.height - this.top - this.height +
  8530. this.props.dot.height / 2, 0) + 'px';
  8531. }
  8532. dot.style.left = props.dot.left + 'px';
  8533. dot.style.top = props.dot.top + 'px';
  8534. }
  8535. };
  8536. /**
  8537. * @constructor ItemPoint
  8538. * @extends Item
  8539. * @param {ItemSet} parent
  8540. * @param {Object} data Object containing parameters start
  8541. * content, className.
  8542. * @param {Object} [options] Options to set initial property values
  8543. * @param {Object} [defaultOptions] default options
  8544. * // TODO: describe available options
  8545. */
  8546. function ItemPoint (parent, data, options, defaultOptions) {
  8547. this.props = {
  8548. dot: {
  8549. top: 0,
  8550. width: 0,
  8551. height: 0
  8552. },
  8553. content: {
  8554. height: 0,
  8555. marginLeft: 0
  8556. }
  8557. };
  8558. Item.call(this, parent, data, options, defaultOptions);
  8559. }
  8560. ItemPoint.prototype = new Item (null, null);
  8561. /**
  8562. * Select the item
  8563. * @override
  8564. */
  8565. ItemPoint.prototype.select = function select() {
  8566. this.selected = true;
  8567. // TODO: select and unselect
  8568. };
  8569. /**
  8570. * Unselect the item
  8571. * @override
  8572. */
  8573. ItemPoint.prototype.unselect = function unselect() {
  8574. this.selected = false;
  8575. // TODO: select and unselect
  8576. };
  8577. /**
  8578. * Repaint the item
  8579. * @return {Boolean} changed
  8580. */
  8581. ItemPoint.prototype.repaint = function repaint() {
  8582. // TODO: make an efficient repaint
  8583. var changed = false;
  8584. var dom = this.dom;
  8585. if (!dom) {
  8586. this._create();
  8587. dom = this.dom;
  8588. changed = true;
  8589. }
  8590. if (dom) {
  8591. if (!this.parent) {
  8592. throw new Error('Cannot repaint item: no parent attached');
  8593. }
  8594. var foreground = this.parent.getForeground();
  8595. if (!foreground) {
  8596. throw new Error('Cannot repaint time axis: ' +
  8597. 'parent has no foreground container element');
  8598. }
  8599. if (!dom.point.parentNode) {
  8600. foreground.appendChild(dom.point);
  8601. foreground.appendChild(dom.point);
  8602. changed = true;
  8603. }
  8604. // update contents
  8605. if (this.data.content != this.content) {
  8606. this.content = this.data.content;
  8607. if (this.content instanceof Element) {
  8608. dom.content.innerHTML = '';
  8609. dom.content.appendChild(this.content);
  8610. }
  8611. else if (this.data.content != undefined) {
  8612. dom.content.innerHTML = this.content;
  8613. }
  8614. else {
  8615. throw new Error('Property "content" missing in item ' + this.data.id);
  8616. }
  8617. changed = true;
  8618. }
  8619. // update class
  8620. var className = (this.data.className? ' ' + this.data.className : '') +
  8621. (this.selected ? ' selected' : '');
  8622. if (this.className != className) {
  8623. this.className = className;
  8624. dom.point.className = 'item point' + className;
  8625. changed = true;
  8626. }
  8627. }
  8628. return changed;
  8629. };
  8630. /**
  8631. * Show the item in the DOM (when not already visible). The items DOM will
  8632. * be created when needed.
  8633. * @return {Boolean} changed
  8634. */
  8635. ItemPoint.prototype.show = function show() {
  8636. if (!this.dom || !this.dom.point.parentNode) {
  8637. return this.repaint();
  8638. }
  8639. else {
  8640. return false;
  8641. }
  8642. };
  8643. /**
  8644. * Hide the item from the DOM (when visible)
  8645. * @return {Boolean} changed
  8646. */
  8647. ItemPoint.prototype.hide = function hide() {
  8648. var changed = false,
  8649. dom = this.dom;
  8650. if (dom) {
  8651. if (dom.point.parentNode) {
  8652. dom.point.parentNode.removeChild(dom.point);
  8653. changed = true;
  8654. }
  8655. }
  8656. return changed;
  8657. };
  8658. /**
  8659. * Reflow the item: calculate its actual size from the DOM
  8660. * @return {boolean} resized returns true if the axis is resized
  8661. * @override
  8662. */
  8663. ItemPoint.prototype.reflow = function reflow() {
  8664. var changed = 0,
  8665. update,
  8666. dom,
  8667. props,
  8668. options,
  8669. margin,
  8670. orientation,
  8671. start,
  8672. top,
  8673. data,
  8674. range;
  8675. if (this.data.start == undefined) {
  8676. throw new Error('Property "start" missing in item ' + this.data.id);
  8677. }
  8678. data = this.data;
  8679. range = this.parent && this.parent.range;
  8680. if (data && range) {
  8681. // TODO: account for the width of the item
  8682. var interval = (range.end - range.start);
  8683. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  8684. }
  8685. else {
  8686. this.visible = false;
  8687. }
  8688. if (this.visible) {
  8689. dom = this.dom;
  8690. if (dom) {
  8691. update = util.updateProperty;
  8692. props = this.props;
  8693. options = this.options;
  8694. orientation = options.orientation || this.defaultOptions.orientation;
  8695. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  8696. start = this.parent.toScreen(this.data.start);
  8697. changed += update(this, 'width', dom.point.offsetWidth);
  8698. changed += update(this, 'height', dom.point.offsetHeight);
  8699. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  8700. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  8701. changed += update(props.content, 'height', dom.content.offsetHeight);
  8702. if (orientation == 'top') {
  8703. top = margin;
  8704. }
  8705. else {
  8706. // default or 'bottom'
  8707. var parentHeight = this.parent.height;
  8708. top = Math.max(parentHeight - this.height - margin, 0);
  8709. }
  8710. changed += update(this, 'top', top);
  8711. changed += update(this, 'left', start - props.dot.width / 2);
  8712. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  8713. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  8714. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  8715. }
  8716. else {
  8717. changed += 1;
  8718. }
  8719. }
  8720. return (changed > 0);
  8721. };
  8722. /**
  8723. * Create an items DOM
  8724. * @private
  8725. */
  8726. ItemPoint.prototype._create = function _create() {
  8727. var dom = this.dom;
  8728. if (!dom) {
  8729. this.dom = dom = {};
  8730. // background box
  8731. dom.point = document.createElement('div');
  8732. // className is updated in repaint()
  8733. // contents box, right from the dot
  8734. dom.content = document.createElement('div');
  8735. dom.content.className = 'content';
  8736. dom.point.appendChild(dom.content);
  8737. // dot at start
  8738. dom.dot = document.createElement('div');
  8739. dom.dot.className = 'dot';
  8740. dom.point.appendChild(dom.dot);
  8741. }
  8742. };
  8743. /**
  8744. * Reposition the item, recalculate its left, top, and width, using the current
  8745. * range and size of the items itemset
  8746. * @override
  8747. */
  8748. ItemPoint.prototype.reposition = function reposition() {
  8749. var dom = this.dom,
  8750. props = this.props;
  8751. if (dom) {
  8752. dom.point.style.top = this.top + 'px';
  8753. dom.point.style.left = this.left + 'px';
  8754. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  8755. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  8756. dom.dot.style.top = props.dot.top + 'px';
  8757. }
  8758. };
  8759. /**
  8760. * @constructor ItemRange
  8761. * @extends Item
  8762. * @param {ItemSet} parent
  8763. * @param {Object} data Object containing parameters start, end
  8764. * content, className.
  8765. * @param {Object} [options] Options to set initial property values
  8766. * @param {Object} [defaultOptions] default options
  8767. * // TODO: describe available options
  8768. */
  8769. function ItemRange (parent, data, options, defaultOptions) {
  8770. this.props = {
  8771. content: {
  8772. left: 0,
  8773. width: 0
  8774. }
  8775. };
  8776. Item.call(this, parent, data, options, defaultOptions);
  8777. }
  8778. ItemRange.prototype = new Item (null, null);
  8779. /**
  8780. * Select the item
  8781. * @override
  8782. */
  8783. ItemRange.prototype.select = function select() {
  8784. this.selected = true;
  8785. // TODO: select and unselect
  8786. };
  8787. /**
  8788. * Unselect the item
  8789. * @override
  8790. */
  8791. ItemRange.prototype.unselect = function unselect() {
  8792. this.selected = false;
  8793. // TODO: select and unselect
  8794. };
  8795. /**
  8796. * Repaint the item
  8797. * @return {Boolean} changed
  8798. */
  8799. ItemRange.prototype.repaint = function repaint() {
  8800. // TODO: make an efficient repaint
  8801. var changed = false;
  8802. var dom = this.dom;
  8803. if (!dom) {
  8804. this._create();
  8805. dom = this.dom;
  8806. changed = true;
  8807. }
  8808. if (dom) {
  8809. if (!this.parent) {
  8810. throw new Error('Cannot repaint item: no parent attached');
  8811. }
  8812. var foreground = this.parent.getForeground();
  8813. if (!foreground) {
  8814. throw new Error('Cannot repaint time axis: ' +
  8815. 'parent has no foreground container element');
  8816. }
  8817. if (!dom.box.parentNode) {
  8818. foreground.appendChild(dom.box);
  8819. changed = true;
  8820. }
  8821. // update content
  8822. if (this.data.content != this.content) {
  8823. this.content = this.data.content;
  8824. if (this.content instanceof Element) {
  8825. dom.content.innerHTML = '';
  8826. dom.content.appendChild(this.content);
  8827. }
  8828. else if (this.data.content != undefined) {
  8829. dom.content.innerHTML = this.content;
  8830. }
  8831. else {
  8832. throw new Error('Property "content" missing in item ' + this.data.id);
  8833. }
  8834. changed = true;
  8835. }
  8836. // update class
  8837. var className = this.data.className ? (' ' + this.data.className) : '';
  8838. if (this.className != className) {
  8839. this.className = className;
  8840. dom.box.className = 'item range' + className;
  8841. changed = true;
  8842. }
  8843. }
  8844. return changed;
  8845. };
  8846. /**
  8847. * Show the item in the DOM (when not already visible). The items DOM will
  8848. * be created when needed.
  8849. * @return {Boolean} changed
  8850. */
  8851. ItemRange.prototype.show = function show() {
  8852. if (!this.dom || !this.dom.box.parentNode) {
  8853. return this.repaint();
  8854. }
  8855. else {
  8856. return false;
  8857. }
  8858. };
  8859. /**
  8860. * Hide the item from the DOM (when visible)
  8861. * @return {Boolean} changed
  8862. */
  8863. ItemRange.prototype.hide = function hide() {
  8864. var changed = false,
  8865. dom = this.dom;
  8866. if (dom) {
  8867. if (dom.box.parentNode) {
  8868. dom.box.parentNode.removeChild(dom.box);
  8869. changed = true;
  8870. }
  8871. }
  8872. return changed;
  8873. };
  8874. /**
  8875. * Reflow the item: calculate its actual size from the DOM
  8876. * @return {boolean} resized returns true if the axis is resized
  8877. * @override
  8878. */
  8879. ItemRange.prototype.reflow = function reflow() {
  8880. var changed = 0,
  8881. dom,
  8882. props,
  8883. options,
  8884. margin,
  8885. padding,
  8886. parent,
  8887. start,
  8888. end,
  8889. data,
  8890. range,
  8891. update,
  8892. box,
  8893. parentWidth,
  8894. contentLeft,
  8895. orientation,
  8896. top;
  8897. if (this.data.start == undefined) {
  8898. throw new Error('Property "start" missing in item ' + this.data.id);
  8899. }
  8900. if (this.data.end == undefined) {
  8901. throw new Error('Property "end" missing in item ' + this.data.id);
  8902. }
  8903. data = this.data;
  8904. range = this.parent && this.parent.range;
  8905. if (data && range) {
  8906. // TODO: account for the width of the item. Take some margin
  8907. this.visible = (data.start < range.end) && (data.end > range.start);
  8908. }
  8909. else {
  8910. this.visible = false;
  8911. }
  8912. if (this.visible) {
  8913. dom = this.dom;
  8914. if (dom) {
  8915. props = this.props;
  8916. options = this.options;
  8917. parent = this.parent;
  8918. start = parent.toScreen(this.data.start);
  8919. end = parent.toScreen(this.data.end);
  8920. update = util.updateProperty;
  8921. box = dom.box;
  8922. parentWidth = parent.width;
  8923. orientation = options.orientation || this.defaultOptions.orientation;
  8924. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  8925. padding = options.padding || this.defaultOptions.padding;
  8926. changed += update(props.content, 'width', dom.content.offsetWidth);
  8927. changed += update(this, 'height', box.offsetHeight);
  8928. // limit the width of the this, as browsers cannot draw very wide divs
  8929. if (start < -parentWidth) {
  8930. start = -parentWidth;
  8931. }
  8932. if (end > 2 * parentWidth) {
  8933. end = 2 * parentWidth;
  8934. }
  8935. // when range exceeds left of the window, position the contents at the left of the visible area
  8936. if (start < 0) {
  8937. contentLeft = Math.min(-start,
  8938. (end - start - props.content.width - 2 * padding));
  8939. // TODO: remove the need for options.padding. it's terrible.
  8940. }
  8941. else {
  8942. contentLeft = 0;
  8943. }
  8944. changed += update(props.content, 'left', contentLeft);
  8945. if (orientation == 'top') {
  8946. top = margin;
  8947. changed += update(this, 'top', top);
  8948. }
  8949. else {
  8950. // default or 'bottom'
  8951. top = parent.height - this.height - margin;
  8952. changed += update(this, 'top', top);
  8953. }
  8954. changed += update(this, 'left', start);
  8955. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  8956. }
  8957. else {
  8958. changed += 1;
  8959. }
  8960. }
  8961. return (changed > 0);
  8962. };
  8963. /**
  8964. * Create an items DOM
  8965. * @private
  8966. */
  8967. ItemRange.prototype._create = function _create() {
  8968. var dom = this.dom;
  8969. if (!dom) {
  8970. this.dom = dom = {};
  8971. // background box
  8972. dom.box = document.createElement('div');
  8973. // className is updated in repaint()
  8974. // contents box
  8975. dom.content = document.createElement('div');
  8976. dom.content.className = 'content';
  8977. dom.box.appendChild(dom.content);
  8978. }
  8979. };
  8980. /**
  8981. * Reposition the item, recalculate its left, top, and width, using the current
  8982. * range and size of the items itemset
  8983. * @override
  8984. */
  8985. ItemRange.prototype.reposition = function reposition() {
  8986. var dom = this.dom,
  8987. props = this.props;
  8988. if (dom) {
  8989. dom.box.style.top = this.top + 'px';
  8990. dom.box.style.left = this.left + 'px';
  8991. dom.box.style.width = this.width + 'px';
  8992. dom.content.style.left = props.content.left + 'px';
  8993. }
  8994. };
  8995. /**
  8996. * @constructor ItemRangeOverflow
  8997. * @extends ItemRange
  8998. * @param {ItemSet} parent
  8999. * @param {Object} data Object containing parameters start, end
  9000. * content, className.
  9001. * @param {Object} [options] Options to set initial property values
  9002. * @param {Object} [defaultOptions] default options
  9003. * // TODO: describe available options
  9004. */
  9005. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  9006. this.props = {
  9007. content: {
  9008. left: 0,
  9009. width: 0
  9010. }
  9011. };
  9012. ItemRange.call(this, parent, data, options, defaultOptions);
  9013. }
  9014. ItemRangeOverflow.prototype = new ItemRange (null, null);
  9015. /**
  9016. * Repaint the item
  9017. * @return {Boolean} changed
  9018. */
  9019. ItemRangeOverflow.prototype.repaint = function repaint() {
  9020. // TODO: make an efficient repaint
  9021. var changed = false;
  9022. var dom = this.dom;
  9023. if (!dom) {
  9024. this._create();
  9025. dom = this.dom;
  9026. changed = true;
  9027. }
  9028. if (dom) {
  9029. if (!this.parent) {
  9030. throw new Error('Cannot repaint item: no parent attached');
  9031. }
  9032. var foreground = this.parent.getForeground();
  9033. if (!foreground) {
  9034. throw new Error('Cannot repaint time axis: ' +
  9035. 'parent has no foreground container element');
  9036. }
  9037. if (!dom.box.parentNode) {
  9038. foreground.appendChild(dom.box);
  9039. changed = true;
  9040. }
  9041. // update content
  9042. if (this.data.content != this.content) {
  9043. this.content = this.data.content;
  9044. if (this.content instanceof Element) {
  9045. dom.content.innerHTML = '';
  9046. dom.content.appendChild(this.content);
  9047. }
  9048. else if (this.data.content != undefined) {
  9049. dom.content.innerHTML = this.content;
  9050. }
  9051. else {
  9052. throw new Error('Property "content" missing in item ' + this.data.id);
  9053. }
  9054. changed = true;
  9055. }
  9056. // update class
  9057. var className = this.data.className ? (' ' + this.data.className) : '';
  9058. if (this.className != className) {
  9059. this.className = className;
  9060. dom.box.className = 'item rangeoverflow' + className;
  9061. changed = true;
  9062. }
  9063. }
  9064. return changed;
  9065. };
  9066. /**
  9067. * Return the items width
  9068. * @return {Integer} width
  9069. */
  9070. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  9071. if (this.props.content !== undefined && this.width < this.props.content.width)
  9072. return this.props.content.width;
  9073. else
  9074. return this.width;
  9075. }
  9076. /**
  9077. * @constructor Group
  9078. * @param {GroupSet} parent
  9079. * @param {Number | String} groupId
  9080. * @param {Object} [options] Options to set initial property values
  9081. * // TODO: describe available options
  9082. * @extends Component
  9083. */
  9084. function Group (parent, groupId, options) {
  9085. this.id = util.randomUUID();
  9086. this.parent = parent;
  9087. this.groupId = groupId;
  9088. this.itemset = null; // ItemSet
  9089. this.options = options || {};
  9090. this.options.top = 0;
  9091. this.props = {
  9092. label: {
  9093. width: 0,
  9094. height: 0
  9095. }
  9096. };
  9097. this.top = 0;
  9098. this.left = 0;
  9099. this.width = 0;
  9100. this.height = 0;
  9101. }
  9102. Group.prototype = new Component();
  9103. // TODO: comment
  9104. Group.prototype.setOptions = Component.prototype.setOptions;
  9105. /**
  9106. * Get the container element of the panel, which can be used by a child to
  9107. * add its own widgets.
  9108. * @returns {HTMLElement} container
  9109. */
  9110. Group.prototype.getContainer = function () {
  9111. return this.parent.getContainer();
  9112. };
  9113. /**
  9114. * Set item set for the group. The group will create a view on the itemset,
  9115. * filtered by the groups id.
  9116. * @param {DataSet | DataView} items
  9117. */
  9118. Group.prototype.setItems = function setItems(items) {
  9119. if (this.itemset) {
  9120. // remove current item set
  9121. this.itemset.hide();
  9122. this.itemset.setItems();
  9123. this.parent.controller.remove(this.itemset);
  9124. this.itemset = null;
  9125. }
  9126. if (items) {
  9127. var groupId = this.groupId;
  9128. var itemsetOptions = Object.create(this.options);
  9129. this.itemset = new ItemSet(this, null, itemsetOptions);
  9130. this.itemset.setRange(this.parent.range);
  9131. this.view = new DataView(items, {
  9132. filter: function (item) {
  9133. return item.group == groupId;
  9134. }
  9135. });
  9136. this.itemset.setItems(this.view);
  9137. this.parent.controller.add(this.itemset);
  9138. }
  9139. };
  9140. /**
  9141. * Repaint the item
  9142. * @return {Boolean} changed
  9143. */
  9144. Group.prototype.repaint = function repaint() {
  9145. return false;
  9146. };
  9147. /**
  9148. * Reflow the item
  9149. * @return {Boolean} resized
  9150. */
  9151. Group.prototype.reflow = function reflow() {
  9152. var changed = 0,
  9153. update = util.updateProperty;
  9154. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  9155. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  9156. // TODO: reckon with the height of the group label
  9157. if (this.label) {
  9158. var inner = this.label.firstChild;
  9159. changed += update(this.props.label, 'width', inner.clientWidth);
  9160. changed += update(this.props.label, 'height', inner.clientHeight);
  9161. }
  9162. else {
  9163. changed += update(this.props.label, 'width', 0);
  9164. changed += update(this.props.label, 'height', 0);
  9165. }
  9166. return (changed > 0);
  9167. };
  9168. /**
  9169. * An GroupSet holds a set of groups
  9170. * @param {Component} parent
  9171. * @param {Component[]} [depends] Components on which this components depends
  9172. * (except for the parent)
  9173. * @param {Object} [options] See GroupSet.setOptions for the available
  9174. * options.
  9175. * @constructor GroupSet
  9176. * @extends Panel
  9177. */
  9178. function GroupSet(parent, depends, options) {
  9179. this.id = util.randomUUID();
  9180. this.parent = parent;
  9181. this.depends = depends;
  9182. this.options = options || {};
  9183. this.range = null; // Range or Object {start: number, end: number}
  9184. this.itemsData = null; // DataSet with items
  9185. this.groupsData = null; // DataSet with groups
  9186. this.groups = {}; // map with groups
  9187. this.dom = {};
  9188. this.props = {
  9189. labels: {
  9190. width: 0
  9191. }
  9192. };
  9193. // TODO: implement right orientation of the labels
  9194. // changes in groups are queued key/value map containing id/action
  9195. this.queue = {};
  9196. var me = this;
  9197. this.listeners = {
  9198. 'add': function (event, params) {
  9199. me._onAdd(params.items);
  9200. },
  9201. 'update': function (event, params) {
  9202. me._onUpdate(params.items);
  9203. },
  9204. 'remove': function (event, params) {
  9205. me._onRemove(params.items);
  9206. }
  9207. };
  9208. }
  9209. GroupSet.prototype = new Panel();
  9210. /**
  9211. * Set options for the GroupSet. Existing options will be extended/overwritten.
  9212. * @param {Object} [options] The following options are available:
  9213. * {String | function} groupsOrder
  9214. * TODO: describe options
  9215. */
  9216. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  9217. GroupSet.prototype.setRange = function (range) {
  9218. // TODO: implement setRange
  9219. };
  9220. /**
  9221. * Set items
  9222. * @param {vis.DataSet | null} items
  9223. */
  9224. GroupSet.prototype.setItems = function setItems(items) {
  9225. this.itemsData = items;
  9226. for (var id in this.groups) {
  9227. if (this.groups.hasOwnProperty(id)) {
  9228. var group = this.groups[id];
  9229. group.setItems(items);
  9230. }
  9231. }
  9232. };
  9233. /**
  9234. * Get items
  9235. * @return {vis.DataSet | null} items
  9236. */
  9237. GroupSet.prototype.getItems = function getItems() {
  9238. return this.itemsData;
  9239. };
  9240. /**
  9241. * Set range (start and end).
  9242. * @param {Range | Object} range A Range or an object containing start and end.
  9243. */
  9244. GroupSet.prototype.setRange = function setRange(range) {
  9245. this.range = range;
  9246. };
  9247. /**
  9248. * Set groups
  9249. * @param {vis.DataSet} groups
  9250. */
  9251. GroupSet.prototype.setGroups = function setGroups(groups) {
  9252. var me = this,
  9253. ids;
  9254. // unsubscribe from current dataset
  9255. if (this.groupsData) {
  9256. util.forEach(this.listeners, function (callback, event) {
  9257. me.groupsData.unsubscribe(event, callback);
  9258. });
  9259. // remove all drawn groups
  9260. ids = this.groupsData.getIds();
  9261. this._onRemove(ids);
  9262. }
  9263. // replace the dataset
  9264. if (!groups) {
  9265. this.groupsData = null;
  9266. }
  9267. else if (groups instanceof DataSet) {
  9268. this.groupsData = groups;
  9269. }
  9270. else {
  9271. this.groupsData = new DataSet({
  9272. convert: {
  9273. start: 'Date',
  9274. end: 'Date'
  9275. }
  9276. });
  9277. this.groupsData.add(groups);
  9278. }
  9279. if (this.groupsData) {
  9280. // subscribe to new dataset
  9281. var id = this.id;
  9282. util.forEach(this.listeners, function (callback, event) {
  9283. me.groupsData.subscribe(event, callback, id);
  9284. });
  9285. // draw all new groups
  9286. ids = this.groupsData.getIds();
  9287. this._onAdd(ids);
  9288. }
  9289. };
  9290. /**
  9291. * Get groups
  9292. * @return {vis.DataSet | null} groups
  9293. */
  9294. GroupSet.prototype.getGroups = function getGroups() {
  9295. return this.groupsData;
  9296. };
  9297. /**
  9298. * Repaint the component
  9299. * @return {Boolean} changed
  9300. */
  9301. GroupSet.prototype.repaint = function repaint() {
  9302. var changed = 0,
  9303. i, id, group, label,
  9304. update = util.updateProperty,
  9305. asSize = util.option.asSize,
  9306. asElement = util.option.asElement,
  9307. options = this.options,
  9308. frame = this.dom.frame,
  9309. labels = this.dom.labels;
  9310. // create frame
  9311. if (!this.parent) {
  9312. throw new Error('Cannot repaint groupset: no parent attached');
  9313. }
  9314. var parentContainer = this.parent.getContainer();
  9315. if (!parentContainer) {
  9316. throw new Error('Cannot repaint groupset: parent has no container element');
  9317. }
  9318. if (!frame) {
  9319. frame = document.createElement('div');
  9320. frame.className = 'groupset';
  9321. this.dom.frame = frame;
  9322. var className = options.className;
  9323. if (className) {
  9324. util.addClassName(frame, util.option.asString(className));
  9325. }
  9326. changed += 1;
  9327. }
  9328. if (!frame.parentNode) {
  9329. parentContainer.appendChild(frame);
  9330. changed += 1;
  9331. }
  9332. // create labels
  9333. var labelContainer = asElement(options.labelContainer);
  9334. if (!labelContainer) {
  9335. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  9336. }
  9337. if (!labels) {
  9338. labels = document.createElement('div');
  9339. labels.className = 'labels';
  9340. //frame.appendChild(labels);
  9341. this.dom.labels = labels;
  9342. }
  9343. if (!labels.parentNode || labels.parentNode != labelContainer) {
  9344. if (labels.parentNode) {
  9345. labels.parentNode.removeChild(labels.parentNode);
  9346. }
  9347. labelContainer.appendChild(labels);
  9348. }
  9349. // reposition frame
  9350. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  9351. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  9352. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  9353. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  9354. // reposition labels
  9355. changed += update(labels.style, 'top', asSize(options.top, '0px'));
  9356. var me = this,
  9357. queue = this.queue,
  9358. groups = this.groups,
  9359. groupsData = this.groupsData;
  9360. // show/hide added/changed/removed groups
  9361. var ids = Object.keys(queue);
  9362. if (ids.length) {
  9363. ids.forEach(function (id) {
  9364. var action = queue[id];
  9365. var group = groups[id];
  9366. //noinspection FallthroughInSwitchStatementJS
  9367. switch (action) {
  9368. case 'add':
  9369. case 'update':
  9370. if (!group) {
  9371. var groupOptions = Object.create(me.options);
  9372. group = new Group(me, id, groupOptions);
  9373. group.setItems(me.itemsData); // attach items data
  9374. groups[id] = group;
  9375. me.controller.add(group);
  9376. }
  9377. // TODO: update group data
  9378. group.data = groupsData.get(id);
  9379. delete queue[id];
  9380. break;
  9381. case 'remove':
  9382. if (group) {
  9383. group.setItems(); // detach items data
  9384. delete groups[id];
  9385. me.controller.remove(group);
  9386. }
  9387. // update lists
  9388. delete queue[id];
  9389. break;
  9390. default:
  9391. console.log('Error: unknown action "' + action + '"');
  9392. }
  9393. });
  9394. // the groupset depends on each of the groups
  9395. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  9396. // TODO: apply dependencies of the groupset
  9397. // update the top positions of the groups in the correct order
  9398. var orderedGroups = this.groupsData.getIds({
  9399. order: this.options.groupsOrder
  9400. });
  9401. for (i = 0; i < orderedGroups.length; i++) {
  9402. (function (group, prevGroup) {
  9403. var top = 0;
  9404. if (prevGroup) {
  9405. top = function () {
  9406. // TODO: top must reckon with options.maxHeight
  9407. return prevGroup.top + prevGroup.height;
  9408. }
  9409. }
  9410. group.setOptions({
  9411. top: top
  9412. });
  9413. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  9414. }
  9415. // (re)create the labels
  9416. while (labels.firstChild) {
  9417. labels.removeChild(labels.firstChild);
  9418. }
  9419. for (i = 0; i < orderedGroups.length; i++) {
  9420. id = orderedGroups[i];
  9421. label = this._createLabel(id);
  9422. labels.appendChild(label);
  9423. }
  9424. changed++;
  9425. }
  9426. // reposition the labels
  9427. // TODO: labels are not displayed correctly when orientation=='top'
  9428. // TODO: width of labelPanel is not immediately updated on a change in groups
  9429. for (id in groups) {
  9430. if (groups.hasOwnProperty(id)) {
  9431. group = groups[id];
  9432. label = group.label;
  9433. if (label) {
  9434. label.style.top = group.top + 'px';
  9435. label.style.height = group.height + 'px';
  9436. }
  9437. }
  9438. }
  9439. return (changed > 0);
  9440. };
  9441. /**
  9442. * Create a label for group with given id
  9443. * @param {Number} id
  9444. * @return {Element} label
  9445. * @private
  9446. */
  9447. GroupSet.prototype._createLabel = function(id) {
  9448. var group = this.groups[id];
  9449. var label = document.createElement('div');
  9450. label.className = 'label';
  9451. var inner = document.createElement('div');
  9452. inner.className = 'inner';
  9453. label.appendChild(inner);
  9454. var content = group.data && group.data.content;
  9455. if (content instanceof Element) {
  9456. inner.appendChild(content);
  9457. }
  9458. else if (content != undefined) {
  9459. inner.innerHTML = content;
  9460. }
  9461. var className = group.data && group.data.className;
  9462. if (className) {
  9463. util.addClassName(label, className);
  9464. }
  9465. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  9466. return label;
  9467. };
  9468. /**
  9469. * Get container element
  9470. * @return {HTMLElement} container
  9471. */
  9472. GroupSet.prototype.getContainer = function getContainer() {
  9473. return this.dom.frame;
  9474. };
  9475. /**
  9476. * Get the width of the group labels
  9477. * @return {Number} width
  9478. */
  9479. GroupSet.prototype.getLabelsWidth = function getContainer() {
  9480. return this.props.labels.width;
  9481. };
  9482. /**
  9483. * Reflow the component
  9484. * @return {Boolean} resized
  9485. */
  9486. GroupSet.prototype.reflow = function reflow() {
  9487. var changed = 0,
  9488. id, group,
  9489. options = this.options,
  9490. update = util.updateProperty,
  9491. asNumber = util.option.asNumber,
  9492. asSize = util.option.asSize,
  9493. frame = this.dom.frame;
  9494. if (frame) {
  9495. var maxHeight = asNumber(options.maxHeight);
  9496. var fixedHeight = (asSize(options.height) != null);
  9497. var height;
  9498. if (fixedHeight) {
  9499. height = frame.offsetHeight;
  9500. }
  9501. else {
  9502. // height is not specified, calculate the sum of the height of all groups
  9503. height = 0;
  9504. for (id in this.groups) {
  9505. if (this.groups.hasOwnProperty(id)) {
  9506. group = this.groups[id];
  9507. height += group.height;
  9508. }
  9509. }
  9510. }
  9511. if (maxHeight != null) {
  9512. height = Math.min(height, maxHeight);
  9513. }
  9514. changed += update(this, 'height', height);
  9515. changed += update(this, 'top', frame.offsetTop);
  9516. changed += update(this, 'left', frame.offsetLeft);
  9517. changed += update(this, 'width', frame.offsetWidth);
  9518. }
  9519. // calculate the maximum width of the labels
  9520. var width = 0;
  9521. for (id in this.groups) {
  9522. if (this.groups.hasOwnProperty(id)) {
  9523. group = this.groups[id];
  9524. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  9525. width = Math.max(width, labelWidth);
  9526. }
  9527. }
  9528. changed += update(this.props.labels, 'width', width);
  9529. return (changed > 0);
  9530. };
  9531. /**
  9532. * Hide the component from the DOM
  9533. * @return {Boolean} changed
  9534. */
  9535. GroupSet.prototype.hide = function hide() {
  9536. if (this.dom.frame && this.dom.frame.parentNode) {
  9537. this.dom.frame.parentNode.removeChild(this.dom.frame);
  9538. return true;
  9539. }
  9540. else {
  9541. return false;
  9542. }
  9543. };
  9544. /**
  9545. * Show the component in the DOM (when not already visible).
  9546. * A repaint will be executed when the component is not visible
  9547. * @return {Boolean} changed
  9548. */
  9549. GroupSet.prototype.show = function show() {
  9550. if (!this.dom.frame || !this.dom.frame.parentNode) {
  9551. return this.repaint();
  9552. }
  9553. else {
  9554. return false;
  9555. }
  9556. };
  9557. /**
  9558. * Handle updated groups
  9559. * @param {Number[]} ids
  9560. * @private
  9561. */
  9562. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  9563. this._toQueue(ids, 'update');
  9564. };
  9565. /**
  9566. * Handle changed groups
  9567. * @param {Number[]} ids
  9568. * @private
  9569. */
  9570. GroupSet.prototype._onAdd = function _onAdd(ids) {
  9571. this._toQueue(ids, 'add');
  9572. };
  9573. /**
  9574. * Handle removed groups
  9575. * @param {Number[]} ids
  9576. * @private
  9577. */
  9578. GroupSet.prototype._onRemove = function _onRemove(ids) {
  9579. this._toQueue(ids, 'remove');
  9580. };
  9581. /**
  9582. * Put groups in the queue to be added/updated/remove
  9583. * @param {Number[]} ids
  9584. * @param {String} action can be 'add', 'update', 'remove'
  9585. */
  9586. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  9587. var queue = this.queue;
  9588. ids.forEach(function (id) {
  9589. queue[id] = action;
  9590. });
  9591. if (this.controller) {
  9592. //this.requestReflow();
  9593. this.requestRepaint();
  9594. }
  9595. };
  9596. /**
  9597. * Create a timeline visualization
  9598. * @param {HTMLElement} container
  9599. * @param {vis.DataSet | Array | DataTable} [items]
  9600. * @param {Object} [options] See Timeline.setOptions for the available options.
  9601. * @constructor
  9602. */
  9603. function Timeline (container, items, options) {
  9604. var me = this;
  9605. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  9606. this.options = {
  9607. orientation: 'bottom',
  9608. min: null,
  9609. max: null,
  9610. zoomMin: 10, // milliseconds
  9611. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  9612. // moveable: true, // TODO: option moveable
  9613. // zoomable: true, // TODO: option zoomable
  9614. showMinorLabels: true,
  9615. showMajorLabels: true,
  9616. showCurrentTime: false,
  9617. showCustomTime: false,
  9618. autoResize: false
  9619. };
  9620. // controller
  9621. this.controller = new Controller();
  9622. // root panel
  9623. if (!container) {
  9624. throw new Error('No container element provided');
  9625. }
  9626. var rootOptions = Object.create(this.options);
  9627. rootOptions.height = function () {
  9628. if (me.options.height) {
  9629. // fixed height
  9630. return me.options.height;
  9631. }
  9632. else {
  9633. // auto height
  9634. return me.timeaxis.height + me.content.height;
  9635. }
  9636. };
  9637. this.rootPanel = new RootPanel(container, rootOptions);
  9638. this.controller.add(this.rootPanel);
  9639. // item panel
  9640. var itemOptions = Object.create(this.options);
  9641. itemOptions.left = function () {
  9642. return me.labelPanel.width;
  9643. };
  9644. itemOptions.width = function () {
  9645. return me.rootPanel.width - me.labelPanel.width;
  9646. };
  9647. itemOptions.top = null;
  9648. itemOptions.height = null;
  9649. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  9650. this.controller.add(this.itemPanel);
  9651. // label panel
  9652. var labelOptions = Object.create(this.options);
  9653. labelOptions.top = null;
  9654. labelOptions.left = null;
  9655. labelOptions.height = null;
  9656. labelOptions.width = function () {
  9657. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  9658. return me.content.getLabelsWidth();
  9659. }
  9660. else {
  9661. return 0;
  9662. }
  9663. };
  9664. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  9665. this.controller.add(this.labelPanel);
  9666. // range
  9667. var rangeOptions = Object.create(this.options);
  9668. this.range = new Range(rangeOptions);
  9669. this.range.setRange(
  9670. now.clone().add('days', -3).valueOf(),
  9671. now.clone().add('days', 4).valueOf()
  9672. );
  9673. // TODO: reckon with options moveable and zoomable
  9674. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  9675. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  9676. this.range.on('rangechange', function () {
  9677. var force = true;
  9678. me.controller.requestReflow(force);
  9679. });
  9680. this.range.on('rangechanged', function () {
  9681. var force = true;
  9682. me.controller.requestReflow(force);
  9683. });
  9684. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  9685. // time axis
  9686. var timeaxisOptions = Object.create(rootOptions);
  9687. timeaxisOptions.range = this.range;
  9688. timeaxisOptions.left = null;
  9689. timeaxisOptions.top = null;
  9690. timeaxisOptions.width = '100%';
  9691. timeaxisOptions.height = null;
  9692. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  9693. this.timeaxis.setRange(this.range);
  9694. this.controller.add(this.timeaxis);
  9695. // current time bar
  9696. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  9697. this.controller.add(this.currenttime);
  9698. // custom time bar
  9699. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  9700. this.controller.add(this.customtime);
  9701. // create itemset or groupset
  9702. this.setGroups(null);
  9703. this.itemsData = null; // DataSet
  9704. this.groupsData = null; // DataSet
  9705. // apply options
  9706. if (options) {
  9707. this.setOptions(options);
  9708. }
  9709. // set data (must be after options are applied)
  9710. if (items) {
  9711. this.setItems(items);
  9712. }
  9713. }
  9714. /**
  9715. * Set options
  9716. * @param {Object} options TODO: describe the available options
  9717. */
  9718. Timeline.prototype.setOptions = function (options) {
  9719. util.extend(this.options, options);
  9720. // force update of range
  9721. // options.start and options.end can be undefined
  9722. //this.range.setRange(options.start, options.end);
  9723. this.range.setRange();
  9724. this.controller.reflow();
  9725. this.controller.repaint();
  9726. };
  9727. /**
  9728. * Set a custom time bar
  9729. * @param {Date} time
  9730. */
  9731. Timeline.prototype.setCustomTime = function (time) {
  9732. this.customtime._setCustomTime(time);
  9733. };
  9734. /**
  9735. * Retrieve the current custom time.
  9736. * @return {Date} customTime
  9737. */
  9738. Timeline.prototype.getCustomTime = function() {
  9739. return new Date(this.customtime.customTime.valueOf());
  9740. };
  9741. /**
  9742. * Set items
  9743. * @param {vis.DataSet | Array | DataTable | null} items
  9744. */
  9745. Timeline.prototype.setItems = function(items) {
  9746. var initialLoad = (this.itemsData == null);
  9747. // convert to type DataSet when needed
  9748. var newItemSet;
  9749. if (!items) {
  9750. newItemSet = null;
  9751. }
  9752. else if (items instanceof DataSet) {
  9753. newItemSet = items;
  9754. }
  9755. if (!(items instanceof DataSet)) {
  9756. newItemSet = new DataSet({
  9757. convert: {
  9758. start: 'Date',
  9759. end: 'Date'
  9760. }
  9761. });
  9762. newItemSet.add(items);
  9763. }
  9764. // set items
  9765. this.itemsData = newItemSet;
  9766. this.content.setItems(newItemSet);
  9767. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  9768. // apply the data range as range
  9769. var dataRange = this.getItemRange();
  9770. // add 5% space on both sides
  9771. var min = dataRange.min;
  9772. var max = dataRange.max;
  9773. if (min != null && max != null) {
  9774. var interval = (max.valueOf() - min.valueOf());
  9775. if (interval <= 0) {
  9776. // prevent an empty interval
  9777. interval = 24 * 60 * 60 * 1000; // 1 day
  9778. }
  9779. min = new Date(min.valueOf() - interval * 0.05);
  9780. max = new Date(max.valueOf() + interval * 0.05);
  9781. }
  9782. // override specified start and/or end date
  9783. if (this.options.start != undefined) {
  9784. min = util.convert(this.options.start, 'Date');
  9785. }
  9786. if (this.options.end != undefined) {
  9787. max = util.convert(this.options.end, 'Date');
  9788. }
  9789. // apply range if there is a min or max available
  9790. if (min != null || max != null) {
  9791. this.range.setRange(min, max);
  9792. }
  9793. }
  9794. };
  9795. /**
  9796. * Set groups
  9797. * @param {vis.DataSet | Array | DataTable} groups
  9798. */
  9799. Timeline.prototype.setGroups = function(groups) {
  9800. var me = this;
  9801. this.groupsData = groups;
  9802. // switch content type between ItemSet or GroupSet when needed
  9803. var type = this.groupsData ? GroupSet : ItemSet;
  9804. if (!(this.content instanceof type)) {
  9805. // remove old content set
  9806. if (this.content) {
  9807. this.content.hide();
  9808. if (this.content.setItems) {
  9809. this.content.setItems(); // disconnect from items
  9810. }
  9811. if (this.content.setGroups) {
  9812. this.content.setGroups(); // disconnect from groups
  9813. }
  9814. this.controller.remove(this.content);
  9815. }
  9816. // create new content set
  9817. var options = Object.create(this.options);
  9818. util.extend(options, {
  9819. top: function () {
  9820. if (me.options.orientation == 'top') {
  9821. return me.timeaxis.height;
  9822. }
  9823. else {
  9824. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  9825. }
  9826. },
  9827. left: null,
  9828. width: '100%',
  9829. height: function () {
  9830. if (me.options.height) {
  9831. return me.itemPanel.height - me.timeaxis.height;
  9832. }
  9833. else {
  9834. return null;
  9835. }
  9836. },
  9837. maxHeight: function () {
  9838. if (me.options.maxHeight) {
  9839. if (!util.isNumber(me.options.maxHeight)) {
  9840. throw new TypeError('Number expected for property maxHeight');
  9841. }
  9842. return me.options.maxHeight - me.timeaxis.height;
  9843. }
  9844. else {
  9845. return null;
  9846. }
  9847. },
  9848. labelContainer: function () {
  9849. return me.labelPanel.getContainer();
  9850. }
  9851. });
  9852. this.content = new type(this.itemPanel, [this.timeaxis], options);
  9853. if (this.content.setRange) {
  9854. this.content.setRange(this.range);
  9855. }
  9856. if (this.content.setItems) {
  9857. this.content.setItems(this.itemsData);
  9858. }
  9859. if (this.content.setGroups) {
  9860. this.content.setGroups(this.groupsData);
  9861. }
  9862. this.controller.add(this.content);
  9863. }
  9864. };
  9865. /**
  9866. * Get the data range of the item set.
  9867. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  9868. * When no minimum is found, min==null
  9869. * When no maximum is found, max==null
  9870. */
  9871. Timeline.prototype.getItemRange = function getItemRange() {
  9872. // calculate min from start filed
  9873. var itemsData = this.itemsData,
  9874. min = null,
  9875. max = null;
  9876. if (itemsData) {
  9877. // calculate the minimum value of the field 'start'
  9878. var minItem = itemsData.min('start');
  9879. min = minItem ? minItem.start.valueOf() : null;
  9880. // calculate maximum value of fields 'start' and 'end'
  9881. var maxStartItem = itemsData.max('start');
  9882. if (maxStartItem) {
  9883. max = maxStartItem.start.valueOf();
  9884. }
  9885. var maxEndItem = itemsData.max('end');
  9886. if (maxEndItem) {
  9887. if (max == null) {
  9888. max = maxEndItem.end.valueOf();
  9889. }
  9890. else {
  9891. max = Math.max(max, maxEndItem.end.valueOf());
  9892. }
  9893. }
  9894. }
  9895. return {
  9896. min: (min != null) ? new Date(min) : null,
  9897. max: (max != null) ? new Date(max) : null
  9898. };
  9899. };
  9900. (function(exports) {
  9901. /**
  9902. * Parse a text source containing data in DOT language into a JSON object.
  9903. * The object contains two lists: one with nodes and one with edges.
  9904. *
  9905. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  9906. *
  9907. * @param {String} data Text containing a graph in DOT-notation
  9908. * @return {Object} graph An object containing two parameters:
  9909. * {Object[]} nodes
  9910. * {Object[]} edges
  9911. */
  9912. function parseDOT (data) {
  9913. dot = data;
  9914. return parseGraph();
  9915. }
  9916. // token types enumeration
  9917. var TOKENTYPE = {
  9918. NULL : 0,
  9919. DELIMITER : 1,
  9920. IDENTIFIER: 2,
  9921. UNKNOWN : 3
  9922. };
  9923. // map with all delimiters
  9924. var DELIMITERS = {
  9925. '{': true,
  9926. '}': true,
  9927. '[': true,
  9928. ']': true,
  9929. ';': true,
  9930. '=': true,
  9931. ',': true,
  9932. '->': true,
  9933. '--': true
  9934. };
  9935. var dot = ''; // current dot file
  9936. var index = 0; // current index in dot file
  9937. var c = ''; // current token character in expr
  9938. var token = ''; // current token
  9939. var tokenType = TOKENTYPE.NULL; // type of the token
  9940. /**
  9941. * Get the first character from the dot file.
  9942. * The character is stored into the char c. If the end of the dot file is
  9943. * reached, the function puts an empty string in c.
  9944. */
  9945. function first() {
  9946. index = 0;
  9947. c = dot.charAt(0);
  9948. }
  9949. /**
  9950. * Get the next character from the dot file.
  9951. * The character is stored into the char c. If the end of the dot file is
  9952. * reached, the function puts an empty string in c.
  9953. */
  9954. function next() {
  9955. index++;
  9956. c = dot.charAt(index);
  9957. }
  9958. /**
  9959. * Preview the next character from the dot file.
  9960. * @return {String} cNext
  9961. */
  9962. function nextPreview() {
  9963. return dot.charAt(index + 1);
  9964. }
  9965. /**
  9966. * Test whether given character is alphabetic or numeric
  9967. * @param {String} c
  9968. * @return {Boolean} isAlphaNumeric
  9969. */
  9970. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  9971. function isAlphaNumeric(c) {
  9972. return regexAlphaNumeric.test(c);
  9973. }
  9974. /**
  9975. * Merge all properties of object b into object b
  9976. * @param {Object} a
  9977. * @param {Object} b
  9978. * @return {Object} a
  9979. */
  9980. function merge (a, b) {
  9981. if (!a) {
  9982. a = {};
  9983. }
  9984. if (b) {
  9985. for (var name in b) {
  9986. if (b.hasOwnProperty(name)) {
  9987. a[name] = b[name];
  9988. }
  9989. }
  9990. }
  9991. return a;
  9992. }
  9993. /**
  9994. * Set a value in an object, where the provided parameter name can be a
  9995. * path with nested parameters. For example:
  9996. *
  9997. * var obj = {a: 2};
  9998. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  9999. *
  10000. * @param {Object} obj
  10001. * @param {String} path A parameter name or dot-separated parameter path,
  10002. * like "color.highlight.border".
  10003. * @param {*} value
  10004. */
  10005. function setValue(obj, path, value) {
  10006. var keys = path.split('.');
  10007. var o = obj;
  10008. while (keys.length) {
  10009. var key = keys.shift();
  10010. if (keys.length) {
  10011. // this isn't the end point
  10012. if (!o[key]) {
  10013. o[key] = {};
  10014. }
  10015. o = o[key];
  10016. }
  10017. else {
  10018. // this is the end point
  10019. o[key] = value;
  10020. }
  10021. }
  10022. }
  10023. /**
  10024. * Add a node to a graph object. If there is already a node with
  10025. * the same id, their attributes will be merged.
  10026. * @param {Object} graph
  10027. * @param {Object} node
  10028. */
  10029. function addNode(graph, node) {
  10030. var i, len;
  10031. var current = null;
  10032. // find root graph (in case of subgraph)
  10033. var graphs = [graph]; // list with all graphs from current graph to root graph
  10034. var root = graph;
  10035. while (root.parent) {
  10036. graphs.push(root.parent);
  10037. root = root.parent;
  10038. }
  10039. // find existing node (at root level) by its id
  10040. if (root.nodes) {
  10041. for (i = 0, len = root.nodes.length; i < len; i++) {
  10042. if (node.id === root.nodes[i].id) {
  10043. current = root.nodes[i];
  10044. break;
  10045. }
  10046. }
  10047. }
  10048. if (!current) {
  10049. // this is a new node
  10050. current = {
  10051. id: node.id
  10052. };
  10053. if (graph.node) {
  10054. // clone default attributes
  10055. current.attr = merge(current.attr, graph.node);
  10056. }
  10057. }
  10058. // add node to this (sub)graph and all its parent graphs
  10059. for (i = graphs.length - 1; i >= 0; i--) {
  10060. var g = graphs[i];
  10061. if (!g.nodes) {
  10062. g.nodes = [];
  10063. }
  10064. if (g.nodes.indexOf(current) == -1) {
  10065. g.nodes.push(current);
  10066. }
  10067. }
  10068. // merge attributes
  10069. if (node.attr) {
  10070. current.attr = merge(current.attr, node.attr);
  10071. }
  10072. }
  10073. /**
  10074. * Add an edge to a graph object
  10075. * @param {Object} graph
  10076. * @param {Object} edge
  10077. */
  10078. function addEdge(graph, edge) {
  10079. if (!graph.edges) {
  10080. graph.edges = [];
  10081. }
  10082. graph.edges.push(edge);
  10083. if (graph.edge) {
  10084. var attr = merge({}, graph.edge); // clone default attributes
  10085. edge.attr = merge(attr, edge.attr); // merge attributes
  10086. }
  10087. }
  10088. /**
  10089. * Create an edge to a graph object
  10090. * @param {Object} graph
  10091. * @param {String | Number | Object} from
  10092. * @param {String | Number | Object} to
  10093. * @param {String} type
  10094. * @param {Object | null} attr
  10095. * @return {Object} edge
  10096. */
  10097. function createEdge(graph, from, to, type, attr) {
  10098. var edge = {
  10099. from: from,
  10100. to: to,
  10101. type: type
  10102. };
  10103. if (graph.edge) {
  10104. edge.attr = merge({}, graph.edge); // clone default attributes
  10105. }
  10106. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  10107. return edge;
  10108. }
  10109. /**
  10110. * Get next token in the current dot file.
  10111. * The token and token type are available as token and tokenType
  10112. */
  10113. function getToken() {
  10114. tokenType = TOKENTYPE.NULL;
  10115. token = '';
  10116. // skip over whitespaces
  10117. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10118. next();
  10119. }
  10120. do {
  10121. var isComment = false;
  10122. // skip comment
  10123. if (c == '#') {
  10124. // find the previous non-space character
  10125. var i = index - 1;
  10126. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  10127. i--;
  10128. }
  10129. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  10130. // the # is at the start of a line, this is indeed a line comment
  10131. while (c != '' && c != '\n') {
  10132. next();
  10133. }
  10134. isComment = true;
  10135. }
  10136. }
  10137. if (c == '/' && nextPreview() == '/') {
  10138. // skip line comment
  10139. while (c != '' && c != '\n') {
  10140. next();
  10141. }
  10142. isComment = true;
  10143. }
  10144. if (c == '/' && nextPreview() == '*') {
  10145. // skip block comment
  10146. while (c != '') {
  10147. if (c == '*' && nextPreview() == '/') {
  10148. // end of block comment found. skip these last two characters
  10149. next();
  10150. next();
  10151. break;
  10152. }
  10153. else {
  10154. next();
  10155. }
  10156. }
  10157. isComment = true;
  10158. }
  10159. // skip over whitespaces
  10160. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10161. next();
  10162. }
  10163. }
  10164. while (isComment);
  10165. // check for end of dot file
  10166. if (c == '') {
  10167. // token is still empty
  10168. tokenType = TOKENTYPE.DELIMITER;
  10169. return;
  10170. }
  10171. // check for delimiters consisting of 2 characters
  10172. var c2 = c + nextPreview();
  10173. if (DELIMITERS[c2]) {
  10174. tokenType = TOKENTYPE.DELIMITER;
  10175. token = c2;
  10176. next();
  10177. next();
  10178. return;
  10179. }
  10180. // check for delimiters consisting of 1 character
  10181. if (DELIMITERS[c]) {
  10182. tokenType = TOKENTYPE.DELIMITER;
  10183. token = c;
  10184. next();
  10185. return;
  10186. }
  10187. // check for an identifier (number or string)
  10188. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  10189. if (isAlphaNumeric(c) || c == '-') {
  10190. token += c;
  10191. next();
  10192. while (isAlphaNumeric(c)) {
  10193. token += c;
  10194. next();
  10195. }
  10196. if (token == 'false') {
  10197. token = false; // convert to boolean
  10198. }
  10199. else if (token == 'true') {
  10200. token = true; // convert to boolean
  10201. }
  10202. else if (!isNaN(Number(token))) {
  10203. token = Number(token); // convert to number
  10204. }
  10205. tokenType = TOKENTYPE.IDENTIFIER;
  10206. return;
  10207. }
  10208. // check for a string enclosed by double quotes
  10209. if (c == '"') {
  10210. next();
  10211. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  10212. token += c;
  10213. if (c == '"') { // skip the escape character
  10214. next();
  10215. }
  10216. next();
  10217. }
  10218. if (c != '"') {
  10219. throw newSyntaxError('End of string " expected');
  10220. }
  10221. next();
  10222. tokenType = TOKENTYPE.IDENTIFIER;
  10223. return;
  10224. }
  10225. // something unknown is found, wrong characters, a syntax error
  10226. tokenType = TOKENTYPE.UNKNOWN;
  10227. while (c != '') {
  10228. token += c;
  10229. next();
  10230. }
  10231. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  10232. }
  10233. /**
  10234. * Parse a graph.
  10235. * @returns {Object} graph
  10236. */
  10237. function parseGraph() {
  10238. var graph = {};
  10239. first();
  10240. getToken();
  10241. // optional strict keyword
  10242. if (token == 'strict') {
  10243. graph.strict = true;
  10244. getToken();
  10245. }
  10246. // graph or digraph keyword
  10247. if (token == 'graph' || token == 'digraph') {
  10248. graph.type = token;
  10249. getToken();
  10250. }
  10251. // optional graph id
  10252. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10253. graph.id = token;
  10254. getToken();
  10255. }
  10256. // open angle bracket
  10257. if (token != '{') {
  10258. throw newSyntaxError('Angle bracket { expected');
  10259. }
  10260. getToken();
  10261. // statements
  10262. parseStatements(graph);
  10263. // close angle bracket
  10264. if (token != '}') {
  10265. throw newSyntaxError('Angle bracket } expected');
  10266. }
  10267. getToken();
  10268. // end of file
  10269. if (token !== '') {
  10270. throw newSyntaxError('End of file expected');
  10271. }
  10272. getToken();
  10273. // remove temporary default properties
  10274. delete graph.node;
  10275. delete graph.edge;
  10276. delete graph.graph;
  10277. return graph;
  10278. }
  10279. /**
  10280. * Parse a list with statements.
  10281. * @param {Object} graph
  10282. */
  10283. function parseStatements (graph) {
  10284. while (token !== '' && token != '}') {
  10285. parseStatement(graph);
  10286. if (token == ';') {
  10287. getToken();
  10288. }
  10289. }
  10290. }
  10291. /**
  10292. * Parse a single statement. Can be a an attribute statement, node
  10293. * statement, a series of node statements and edge statements, or a
  10294. * parameter.
  10295. * @param {Object} graph
  10296. */
  10297. function parseStatement(graph) {
  10298. // parse subgraph
  10299. var subgraph = parseSubgraph(graph);
  10300. if (subgraph) {
  10301. // edge statements
  10302. parseEdge(graph, subgraph);
  10303. return;
  10304. }
  10305. // parse an attribute statement
  10306. var attr = parseAttributeStatement(graph);
  10307. if (attr) {
  10308. return;
  10309. }
  10310. // parse node
  10311. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10312. throw newSyntaxError('Identifier expected');
  10313. }
  10314. var id = token; // id can be a string or a number
  10315. getToken();
  10316. if (token == '=') {
  10317. // id statement
  10318. getToken();
  10319. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10320. throw newSyntaxError('Identifier expected');
  10321. }
  10322. graph[id] = token;
  10323. getToken();
  10324. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  10325. }
  10326. else {
  10327. parseNodeStatement(graph, id);
  10328. }
  10329. }
  10330. /**
  10331. * Parse a subgraph
  10332. * @param {Object} graph parent graph object
  10333. * @return {Object | null} subgraph
  10334. */
  10335. function parseSubgraph (graph) {
  10336. var subgraph = null;
  10337. // optional subgraph keyword
  10338. if (token == 'subgraph') {
  10339. subgraph = {};
  10340. subgraph.type = 'subgraph';
  10341. getToken();
  10342. // optional graph id
  10343. if (tokenType == TOKENTYPE.IDENTIFIER) {
  10344. subgraph.id = token;
  10345. getToken();
  10346. }
  10347. }
  10348. // open angle bracket
  10349. if (token == '{') {
  10350. getToken();
  10351. if (!subgraph) {
  10352. subgraph = {};
  10353. }
  10354. subgraph.parent = graph;
  10355. subgraph.node = graph.node;
  10356. subgraph.edge = graph.edge;
  10357. subgraph.graph = graph.graph;
  10358. // statements
  10359. parseStatements(subgraph);
  10360. // close angle bracket
  10361. if (token != '}') {
  10362. throw newSyntaxError('Angle bracket } expected');
  10363. }
  10364. getToken();
  10365. // remove temporary default properties
  10366. delete subgraph.node;
  10367. delete subgraph.edge;
  10368. delete subgraph.graph;
  10369. delete subgraph.parent;
  10370. // register at the parent graph
  10371. if (!graph.subgraphs) {
  10372. graph.subgraphs = [];
  10373. }
  10374. graph.subgraphs.push(subgraph);
  10375. }
  10376. return subgraph;
  10377. }
  10378. /**
  10379. * parse an attribute statement like "node [shape=circle fontSize=16]".
  10380. * Available keywords are 'node', 'edge', 'graph'.
  10381. * The previous list with default attributes will be replaced
  10382. * @param {Object} graph
  10383. * @returns {String | null} keyword Returns the name of the parsed attribute
  10384. * (node, edge, graph), or null if nothing
  10385. * is parsed.
  10386. */
  10387. function parseAttributeStatement (graph) {
  10388. // attribute statements
  10389. if (token == 'node') {
  10390. getToken();
  10391. // node attributes
  10392. graph.node = parseAttributeList();
  10393. return 'node';
  10394. }
  10395. else if (token == 'edge') {
  10396. getToken();
  10397. // edge attributes
  10398. graph.edge = parseAttributeList();
  10399. return 'edge';
  10400. }
  10401. else if (token == 'graph') {
  10402. getToken();
  10403. // graph attributes
  10404. graph.graph = parseAttributeList();
  10405. return 'graph';
  10406. }
  10407. return null;
  10408. }
  10409. /**
  10410. * parse a node statement
  10411. * @param {Object} graph
  10412. * @param {String | Number} id
  10413. */
  10414. function parseNodeStatement(graph, id) {
  10415. // node statement
  10416. var node = {
  10417. id: id
  10418. };
  10419. var attr = parseAttributeList();
  10420. if (attr) {
  10421. node.attr = attr;
  10422. }
  10423. addNode(graph, node);
  10424. // edge statements
  10425. parseEdge(graph, id);
  10426. }
  10427. /**
  10428. * Parse an edge or a series of edges
  10429. * @param {Object} graph
  10430. * @param {String | Number} from Id of the from node
  10431. */
  10432. function parseEdge(graph, from) {
  10433. while (token == '->' || token == '--') {
  10434. var to;
  10435. var type = token;
  10436. getToken();
  10437. var subgraph = parseSubgraph(graph);
  10438. if (subgraph) {
  10439. to = subgraph;
  10440. }
  10441. else {
  10442. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10443. throw newSyntaxError('Identifier or subgraph expected');
  10444. }
  10445. to = token;
  10446. addNode(graph, {
  10447. id: to
  10448. });
  10449. getToken();
  10450. }
  10451. // parse edge attributes
  10452. var attr = parseAttributeList();
  10453. // create edge
  10454. var edge = createEdge(graph, from, to, type, attr);
  10455. addEdge(graph, edge);
  10456. from = to;
  10457. }
  10458. }
  10459. /**
  10460. * Parse a set with attributes,
  10461. * for example [label="1.000", shape=solid]
  10462. * @return {Object | null} attr
  10463. */
  10464. function parseAttributeList() {
  10465. var attr = null;
  10466. while (token == '[') {
  10467. getToken();
  10468. attr = {};
  10469. while (token !== '' && token != ']') {
  10470. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10471. throw newSyntaxError('Attribute name expected');
  10472. }
  10473. var name = token;
  10474. getToken();
  10475. if (token != '=') {
  10476. throw newSyntaxError('Equal sign = expected');
  10477. }
  10478. getToken();
  10479. if (tokenType != TOKENTYPE.IDENTIFIER) {
  10480. throw newSyntaxError('Attribute value expected');
  10481. }
  10482. var value = token;
  10483. setValue(attr, name, value); // name can be a path
  10484. getToken();
  10485. if (token ==',') {
  10486. getToken();
  10487. }
  10488. }
  10489. if (token != ']') {
  10490. throw newSyntaxError('Bracket ] expected');
  10491. }
  10492. getToken();
  10493. }
  10494. return attr;
  10495. }
  10496. /**
  10497. * Create a syntax error with extra information on current token and index.
  10498. * @param {String} message
  10499. * @returns {SyntaxError} err
  10500. */
  10501. function newSyntaxError(message) {
  10502. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  10503. }
  10504. /**
  10505. * Chop off text after a maximum length
  10506. * @param {String} text
  10507. * @param {Number} maxLength
  10508. * @returns {String}
  10509. */
  10510. function chop (text, maxLength) {
  10511. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  10512. }
  10513. /**
  10514. * Execute a function fn for each pair of elements in two arrays
  10515. * @param {Array | *} array1
  10516. * @param {Array | *} array2
  10517. * @param {function} fn
  10518. */
  10519. function forEach2(array1, array2, fn) {
  10520. if (array1 instanceof Array) {
  10521. array1.forEach(function (elem1) {
  10522. if (array2 instanceof Array) {
  10523. array2.forEach(function (elem2) {
  10524. fn(elem1, elem2);
  10525. });
  10526. }
  10527. else {
  10528. fn(elem1, array2);
  10529. }
  10530. });
  10531. }
  10532. else {
  10533. if (array2 instanceof Array) {
  10534. array2.forEach(function (elem2) {
  10535. fn(array1, elem2);
  10536. });
  10537. }
  10538. else {
  10539. fn(array1, array2);
  10540. }
  10541. }
  10542. }
  10543. /**
  10544. * Convert a string containing a graph in DOT language into a map containing
  10545. * with nodes and edges in the format of graph.
  10546. * @param {String} data Text containing a graph in DOT-notation
  10547. * @return {Object} graphData
  10548. */
  10549. function DOTToGraph (data) {
  10550. // parse the DOT file
  10551. var dotData = parseDOT(data);
  10552. var graphData = {
  10553. nodes: [],
  10554. edges: [],
  10555. options: {}
  10556. };
  10557. // copy the nodes
  10558. if (dotData.nodes) {
  10559. dotData.nodes.forEach(function (dotNode) {
  10560. var graphNode = {
  10561. id: dotNode.id,
  10562. label: String(dotNode.label || dotNode.id)
  10563. };
  10564. merge(graphNode, dotNode.attr);
  10565. if (graphNode.image) {
  10566. graphNode.shape = 'image';
  10567. }
  10568. graphData.nodes.push(graphNode);
  10569. });
  10570. }
  10571. // copy the edges
  10572. if (dotData.edges) {
  10573. /**
  10574. * Convert an edge in DOT format to an edge with VisGraph format
  10575. * @param {Object} dotEdge
  10576. * @returns {Object} graphEdge
  10577. */
  10578. function convertEdge(dotEdge) {
  10579. var graphEdge = {
  10580. from: dotEdge.from,
  10581. to: dotEdge.to
  10582. };
  10583. merge(graphEdge, dotEdge.attr);
  10584. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  10585. return graphEdge;
  10586. }
  10587. dotData.edges.forEach(function (dotEdge) {
  10588. var from, to;
  10589. if (dotEdge.from instanceof Object) {
  10590. from = dotEdge.from.nodes;
  10591. }
  10592. else {
  10593. from = {
  10594. id: dotEdge.from
  10595. }
  10596. }
  10597. if (dotEdge.to instanceof Object) {
  10598. to = dotEdge.to.nodes;
  10599. }
  10600. else {
  10601. to = {
  10602. id: dotEdge.to
  10603. }
  10604. }
  10605. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  10606. dotEdge.from.edges.forEach(function (subEdge) {
  10607. var graphEdge = convertEdge(subEdge);
  10608. graphData.edges.push(graphEdge);
  10609. });
  10610. }
  10611. forEach2(from, to, function (from, to) {
  10612. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  10613. var graphEdge = convertEdge(subEdge);
  10614. graphData.edges.push(graphEdge);
  10615. });
  10616. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  10617. dotEdge.to.edges.forEach(function (subEdge) {
  10618. var graphEdge = convertEdge(subEdge);
  10619. graphData.edges.push(graphEdge);
  10620. });
  10621. }
  10622. });
  10623. }
  10624. // copy the options
  10625. if (dotData.attr) {
  10626. graphData.options = dotData.attr;
  10627. }
  10628. return graphData;
  10629. }
  10630. // exports
  10631. exports.parseDOT = parseDOT;
  10632. exports.DOTToGraph = DOTToGraph;
  10633. })(typeof util !== 'undefined' ? util : exports);
  10634. /**
  10635. * Canvas shapes used by the Graph
  10636. */
  10637. if (typeof CanvasRenderingContext2D !== 'undefined') {
  10638. /**
  10639. * Draw a circle shape
  10640. */
  10641. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  10642. this.beginPath();
  10643. this.arc(x, y, r, 0, 2*Math.PI, false);
  10644. };
  10645. /**
  10646. * Draw a square shape
  10647. * @param {Number} x horizontal center
  10648. * @param {Number} y vertical center
  10649. * @param {Number} r size, width and height of the square
  10650. */
  10651. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  10652. this.beginPath();
  10653. this.rect(x - r, y - r, r * 2, r * 2);
  10654. };
  10655. /**
  10656. * Draw a triangle shape
  10657. * @param {Number} x horizontal center
  10658. * @param {Number} y vertical center
  10659. * @param {Number} r radius, half the length of the sides of the triangle
  10660. */
  10661. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  10662. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10663. this.beginPath();
  10664. var s = r * 2;
  10665. var s2 = s / 2;
  10666. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10667. var h = Math.sqrt(s * s - s2 * s2); // height
  10668. this.moveTo(x, y - (h - ir));
  10669. this.lineTo(x + s2, y + ir);
  10670. this.lineTo(x - s2, y + ir);
  10671. this.lineTo(x, y - (h - ir));
  10672. this.closePath();
  10673. };
  10674. /**
  10675. * Draw a triangle shape in downward orientation
  10676. * @param {Number} x horizontal center
  10677. * @param {Number} y vertical center
  10678. * @param {Number} r radius
  10679. */
  10680. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  10681. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10682. this.beginPath();
  10683. var s = r * 2;
  10684. var s2 = s / 2;
  10685. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10686. var h = Math.sqrt(s * s - s2 * s2); // height
  10687. this.moveTo(x, y + (h - ir));
  10688. this.lineTo(x + s2, y - ir);
  10689. this.lineTo(x - s2, y - ir);
  10690. this.lineTo(x, y + (h - ir));
  10691. this.closePath();
  10692. };
  10693. /**
  10694. * Draw a star shape, a star with 5 points
  10695. * @param {Number} x horizontal center
  10696. * @param {Number} y vertical center
  10697. * @param {Number} r radius, half the length of the sides of the triangle
  10698. */
  10699. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  10700. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  10701. this.beginPath();
  10702. for (var n = 0; n < 10; n++) {
  10703. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  10704. this.lineTo(
  10705. x + radius * Math.sin(n * 2 * Math.PI / 10),
  10706. y - radius * Math.cos(n * 2 * Math.PI / 10)
  10707. );
  10708. }
  10709. this.closePath();
  10710. };
  10711. /**
  10712. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  10713. */
  10714. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  10715. var r2d = Math.PI/180;
  10716. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  10717. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  10718. this.beginPath();
  10719. this.moveTo(x+r,y);
  10720. this.lineTo(x+w-r,y);
  10721. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  10722. this.lineTo(x+w,y+h-r);
  10723. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  10724. this.lineTo(x+r,y+h);
  10725. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  10726. this.lineTo(x,y+r);
  10727. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  10728. };
  10729. /**
  10730. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10731. */
  10732. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  10733. var kappa = .5522848,
  10734. ox = (w / 2) * kappa, // control point offset horizontal
  10735. oy = (h / 2) * kappa, // control point offset vertical
  10736. xe = x + w, // x-end
  10737. ye = y + h, // y-end
  10738. xm = x + w / 2, // x-middle
  10739. ym = y + h / 2; // y-middle
  10740. this.beginPath();
  10741. this.moveTo(x, ym);
  10742. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10743. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10744. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10745. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10746. };
  10747. /**
  10748. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10749. */
  10750. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  10751. var f = 1/3;
  10752. var wEllipse = w;
  10753. var hEllipse = h * f;
  10754. var kappa = .5522848,
  10755. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  10756. oy = (hEllipse / 2) * kappa, // control point offset vertical
  10757. xe = x + wEllipse, // x-end
  10758. ye = y + hEllipse, // y-end
  10759. xm = x + wEllipse / 2, // x-middle
  10760. ym = y + hEllipse / 2, // y-middle
  10761. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  10762. yeb = y + h; // y-end, bottom ellipse
  10763. this.beginPath();
  10764. this.moveTo(xe, ym);
  10765. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10766. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10767. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10768. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10769. this.lineTo(xe, ymb);
  10770. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  10771. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  10772. this.lineTo(x, ym);
  10773. };
  10774. /**
  10775. * Draw an arrow point (no line)
  10776. */
  10777. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  10778. // tail
  10779. var xt = x - length * Math.cos(angle);
  10780. var yt = y - length * Math.sin(angle);
  10781. // inner tail
  10782. // TODO: allow to customize different shapes
  10783. var xi = x - length * 0.9 * Math.cos(angle);
  10784. var yi = y - length * 0.9 * Math.sin(angle);
  10785. // left
  10786. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  10787. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  10788. // right
  10789. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  10790. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  10791. this.beginPath();
  10792. this.moveTo(x, y);
  10793. this.lineTo(xl, yl);
  10794. this.lineTo(xi, yi);
  10795. this.lineTo(xr, yr);
  10796. this.closePath();
  10797. };
  10798. /**
  10799. * Sets up the dashedLine functionality for drawing
  10800. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  10801. * @author David Jordan
  10802. * @date 2012-08-08
  10803. */
  10804. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  10805. if (!dashArray) dashArray=[10,5];
  10806. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  10807. var dashCount = dashArray.length;
  10808. this.moveTo(x, y);
  10809. var dx = (x2-x), dy = (y2-y);
  10810. var slope = dy/dx;
  10811. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  10812. var dashIndex=0, draw=true;
  10813. while (distRemaining>=0.1){
  10814. var dashLength = dashArray[dashIndex++%dashCount];
  10815. if (dashLength > distRemaining) dashLength = distRemaining;
  10816. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  10817. if (dx<0) xStep = -xStep;
  10818. x += xStep;
  10819. y += slope*xStep;
  10820. this[draw ? 'lineTo' : 'moveTo'](x,y);
  10821. distRemaining -= dashLength;
  10822. draw = !draw;
  10823. }
  10824. };
  10825. // TODO: add diamond shape
  10826. }
  10827. /**
  10828. * @class Node
  10829. * A node. A node can be connected to other nodes via one or multiple edges.
  10830. * @param {object} properties An object containing properties for the node. All
  10831. * properties are optional, except for the id.
  10832. * {number} id Id of the node. Required
  10833. * {string} label Text label for the node
  10834. * {number} x Horizontal position of the node
  10835. * {number} y Vertical position of the node
  10836. * {string} shape Node shape, available:
  10837. * "database", "circle", "ellipse",
  10838. * "box", "image", "text", "dot",
  10839. * "star", "triangle", "triangleDown",
  10840. * "square"
  10841. * {string} image An image url
  10842. * {string} title An title text, can be HTML
  10843. * {anytype} group A group name or number
  10844. * @param {Graph.Images} imagelist A list with images. Only needed
  10845. * when the node has an image
  10846. * @param {Graph.Groups} grouplist A list with groups. Needed for
  10847. * retrieving group properties
  10848. * @param {Object} constants An object with default values for
  10849. * example for the color
  10850. */
  10851. function Node(properties, imagelist, grouplist, constants) {
  10852. this.selected = false;
  10853. this.edges = []; // all edges connected to this node
  10854. this.group = constants.nodes.group;
  10855. this.fontSize = constants.nodes.fontSize;
  10856. this.fontFace = constants.nodes.fontFace;
  10857. this.fontColor = constants.nodes.fontColor;
  10858. this.color = constants.nodes.color;
  10859. // set defaults for the properties
  10860. this.id = undefined;
  10861. this.shape = constants.nodes.shape;
  10862. this.image = constants.nodes.image;
  10863. this.x = 0;
  10864. this.y = 0;
  10865. this.xFixed = false;
  10866. this.yFixed = false;
  10867. this.radius = constants.nodes.radius;
  10868. this.radiusFixed = false;
  10869. this.radiusMin = constants.nodes.radiusMin;
  10870. this.radiusMax = constants.nodes.radiusMax;
  10871. this.imagelist = imagelist;
  10872. this.grouplist = grouplist;
  10873. this.setProperties(properties, constants);
  10874. // mass, force, velocity
  10875. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  10876. this.fx = 0.0; // external force x
  10877. this.fy = 0.0; // external force y
  10878. this.vx = 0.0; // velocity x
  10879. this.vy = 0.0; // velocity y
  10880. this.minForce = constants.minForce;
  10881. this.damping = 0.9; // damping factor
  10882. };
  10883. /**
  10884. * Attach a edge to the node
  10885. * @param {Edge} edge
  10886. */
  10887. Node.prototype.attachEdge = function(edge) {
  10888. if (this.edges.indexOf(edge) == -1) {
  10889. this.edges.push(edge);
  10890. }
  10891. this._updateMass();
  10892. };
  10893. /**
  10894. * Detach a edge from the node
  10895. * @param {Edge} edge
  10896. */
  10897. Node.prototype.detachEdge = function(edge) {
  10898. var index = this.edges.indexOf(edge);
  10899. if (index != -1) {
  10900. this.edges.splice(index, 1);
  10901. }
  10902. this._updateMass();
  10903. };
  10904. /**
  10905. * Update the nodes mass, which is determined by the number of edges connecting
  10906. * to it (more edges -> heavier node).
  10907. * @private
  10908. */
  10909. Node.prototype._updateMass = function() {
  10910. this.mass = 50 + 20 * this.edges.length; // kg
  10911. };
  10912. /**
  10913. * Set or overwrite properties for the node
  10914. * @param {Object} properties an object with properties
  10915. * @param {Object} constants and object with default, global properties
  10916. */
  10917. Node.prototype.setProperties = function(properties, constants) {
  10918. if (!properties) {
  10919. return;
  10920. }
  10921. // basic properties
  10922. if (properties.id != undefined) {this.id = properties.id;}
  10923. if (properties.label != undefined) {this.label = properties.label;}
  10924. if (properties.title != undefined) {this.title = properties.title;}
  10925. if (properties.group != undefined) {this.group = properties.group;}
  10926. if (properties.x != undefined) {this.x = properties.x;}
  10927. if (properties.y != undefined) {this.y = properties.y;}
  10928. if (properties.value != undefined) {this.value = properties.value;}
  10929. if (this.id === undefined) {
  10930. throw "Node must have an id";
  10931. }
  10932. // copy group properties
  10933. if (this.group) {
  10934. var groupObj = this.grouplist.get(this.group);
  10935. for (var prop in groupObj) {
  10936. if (groupObj.hasOwnProperty(prop)) {
  10937. this[prop] = groupObj[prop];
  10938. }
  10939. }
  10940. }
  10941. // individual shape properties
  10942. if (properties.shape != undefined) {this.shape = properties.shape;}
  10943. if (properties.image != undefined) {this.image = properties.image;}
  10944. if (properties.radius != undefined) {this.radius = properties.radius;}
  10945. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  10946. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  10947. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  10948. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  10949. if (this.image != undefined) {
  10950. if (this.imagelist) {
  10951. this.imageObj = this.imagelist.load(this.image);
  10952. }
  10953. else {
  10954. throw "No imagelist provided";
  10955. }
  10956. }
  10957. this.xFixed = this.xFixed || (properties.x != undefined);
  10958. this.yFixed = this.yFixed || (properties.y != undefined);
  10959. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  10960. if (this.shape == 'image') {
  10961. this.radiusMin = constants.nodes.widthMin;
  10962. this.radiusMax = constants.nodes.widthMax;
  10963. }
  10964. // choose draw method depending on the shape
  10965. switch (this.shape) {
  10966. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  10967. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  10968. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  10969. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10970. // TODO: add diamond shape
  10971. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  10972. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  10973. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  10974. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  10975. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  10976. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  10977. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  10978. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10979. }
  10980. // reset the size of the node, this can be changed
  10981. this._reset();
  10982. };
  10983. /**
  10984. * Parse a color property into an object with border, background, and
  10985. * hightlight colors
  10986. * @param {Object | String} color
  10987. * @return {Object} colorObject
  10988. */
  10989. Node.parseColor = function(color) {
  10990. var c;
  10991. if (util.isString(color)) {
  10992. c = {
  10993. border: color,
  10994. background: color,
  10995. highlight: {
  10996. border: color,
  10997. background: color
  10998. }
  10999. };
  11000. // TODO: automatically generate a nice highlight color
  11001. }
  11002. else {
  11003. c = {};
  11004. c.background = color.background || 'white';
  11005. c.border = color.border || c.background;
  11006. if (util.isString(color.highlight)) {
  11007. c.highlight = {
  11008. border: color.highlight,
  11009. background: color.highlight
  11010. }
  11011. }
  11012. else {
  11013. c.highlight = {};
  11014. c.highlight.background = color.highlight && color.highlight.background || c.background;
  11015. c.highlight.border = color.highlight && color.highlight.border || c.border;
  11016. }
  11017. }
  11018. return c;
  11019. };
  11020. /**
  11021. * select this node
  11022. */
  11023. Node.prototype.select = function() {
  11024. this.selected = true;
  11025. this._reset();
  11026. };
  11027. /**
  11028. * unselect this node
  11029. */
  11030. Node.prototype.unselect = function() {
  11031. this.selected = false;
  11032. this._reset();
  11033. };
  11034. /**
  11035. * Reset the calculated size of the node, forces it to recalculate its size
  11036. * @private
  11037. */
  11038. Node.prototype._reset = function() {
  11039. this.width = undefined;
  11040. this.height = undefined;
  11041. };
  11042. /**
  11043. * get the title of this node.
  11044. * @return {string} title The title of the node, or undefined when no title
  11045. * has been set.
  11046. */
  11047. Node.prototype.getTitle = function() {
  11048. return this.title;
  11049. };
  11050. /**
  11051. * Calculate the distance to the border of the Node
  11052. * @param {CanvasRenderingContext2D} ctx
  11053. * @param {Number} angle Angle in radians
  11054. * @returns {number} distance Distance to the border in pixels
  11055. */
  11056. Node.prototype.distanceToBorder = function (ctx, angle) {
  11057. var borderWidth = 1;
  11058. if (!this.width) {
  11059. this.resize(ctx);
  11060. }
  11061. //noinspection FallthroughInSwitchStatementJS
  11062. switch (this.shape) {
  11063. case 'circle':
  11064. case 'dot':
  11065. return this.radius + borderWidth;
  11066. case 'ellipse':
  11067. var a = this.width / 2;
  11068. var b = this.height / 2;
  11069. var w = (Math.sin(angle) * a);
  11070. var h = (Math.cos(angle) * b);
  11071. return a * b / Math.sqrt(w * w + h * h);
  11072. // TODO: implement distanceToBorder for database
  11073. // TODO: implement distanceToBorder for triangle
  11074. // TODO: implement distanceToBorder for triangleDown
  11075. case 'box':
  11076. case 'image':
  11077. case 'text':
  11078. default:
  11079. if (this.width) {
  11080. return Math.min(
  11081. Math.abs(this.width / 2 / Math.cos(angle)),
  11082. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  11083. // TODO: reckon with border radius too in case of box
  11084. }
  11085. else {
  11086. return 0;
  11087. }
  11088. }
  11089. // TODO: implement calculation of distance to border for all shapes
  11090. };
  11091. /**
  11092. * Set forces acting on the node
  11093. * @param {number} fx Force in horizontal direction
  11094. * @param {number} fy Force in vertical direction
  11095. */
  11096. Node.prototype._setForce = function(fx, fy) {
  11097. this.fx = fx;
  11098. this.fy = fy;
  11099. };
  11100. /**
  11101. * Add forces acting on the node
  11102. * @param {number} fx Force in horizontal direction
  11103. * @param {number} fy Force in vertical direction
  11104. * @private
  11105. */
  11106. Node.prototype._addForce = function(fx, fy) {
  11107. this.fx += fx;
  11108. this.fy += fy;
  11109. };
  11110. /**
  11111. * Perform one discrete step for the node
  11112. * @param {number} interval Time interval in seconds
  11113. */
  11114. Node.prototype.discreteStep = function(interval) {
  11115. if (!this.xFixed) {
  11116. var dx = -this.damping * this.vx; // damping force
  11117. var ax = (this.fx + dx) / this.mass; // acceleration
  11118. this.vx += ax / interval; // velocity
  11119. this.x += this.vx / interval; // position
  11120. }
  11121. if (!this.yFixed) {
  11122. var dy = -this.damping * this.vy; // damping force
  11123. var ay = (this.fy + dy) / this.mass; // acceleration
  11124. this.vy += ay / interval; // velocity
  11125. this.y += this.vy / interval; // position
  11126. }
  11127. };
  11128. /**
  11129. * Check if this node has a fixed x and y position
  11130. * @return {boolean} true if fixed, false if not
  11131. */
  11132. Node.prototype.isFixed = function() {
  11133. return (this.xFixed && this.yFixed);
  11134. };
  11135. /**
  11136. * Check if this node is moving
  11137. * @param {number} vmin the minimum velocity considered as "moving"
  11138. * @return {boolean} true if moving, false if it has no velocity
  11139. */
  11140. // TODO: replace this method with calculating the kinetic energy
  11141. Node.prototype.isMoving = function(vmin) {
  11142. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  11143. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  11144. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  11145. };
  11146. /**
  11147. * check if this node is selecte
  11148. * @return {boolean} selected True if node is selected, else false
  11149. */
  11150. Node.prototype.isSelected = function() {
  11151. return this.selected;
  11152. };
  11153. /**
  11154. * Retrieve the value of the node. Can be undefined
  11155. * @return {Number} value
  11156. */
  11157. Node.prototype.getValue = function() {
  11158. return this.value;
  11159. };
  11160. /**
  11161. * Calculate the distance from the nodes location to the given location (x,y)
  11162. * @param {Number} x
  11163. * @param {Number} y
  11164. * @return {Number} value
  11165. */
  11166. Node.prototype.getDistance = function(x, y) {
  11167. var dx = this.x - x,
  11168. dy = this.y - y;
  11169. return Math.sqrt(dx * dx + dy * dy);
  11170. };
  11171. /**
  11172. * Adjust the value range of the node. The node will adjust it's radius
  11173. * based on its value.
  11174. * @param {Number} min
  11175. * @param {Number} max
  11176. */
  11177. Node.prototype.setValueRange = function(min, max) {
  11178. if (!this.radiusFixed && this.value !== undefined) {
  11179. if (max == min) {
  11180. this.radius = (this.radiusMin + this.radiusMax) / 2;
  11181. }
  11182. else {
  11183. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  11184. this.radius = (this.value - min) * scale + this.radiusMin;
  11185. }
  11186. }
  11187. };
  11188. /**
  11189. * Draw this node in the given canvas
  11190. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11191. * @param {CanvasRenderingContext2D} ctx
  11192. */
  11193. Node.prototype.draw = function(ctx) {
  11194. throw "Draw method not initialized for node";
  11195. };
  11196. /**
  11197. * Recalculate the size of this node in the given canvas
  11198. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11199. * @param {CanvasRenderingContext2D} ctx
  11200. */
  11201. Node.prototype.resize = function(ctx) {
  11202. throw "Resize method not initialized for node";
  11203. };
  11204. /**
  11205. * Check if this object is overlapping with the provided object
  11206. * @param {Object} obj an object with parameters left, top, right, bottom
  11207. * @return {boolean} True if location is located on node
  11208. */
  11209. Node.prototype.isOverlappingWith = function(obj) {
  11210. return (this.left < obj.right &&
  11211. this.left + this.width > obj.left &&
  11212. this.top < obj.bottom &&
  11213. this.top + this.height > obj.top);
  11214. };
  11215. Node.prototype._resizeImage = function (ctx) {
  11216. // TODO: pre calculate the image size
  11217. if (!this.width) { // undefined or 0
  11218. var width, height;
  11219. if (this.value) {
  11220. var scale = this.imageObj.height / this.imageObj.width;
  11221. width = this.radius || this.imageObj.width;
  11222. height = this.radius * scale || this.imageObj.height;
  11223. }
  11224. else {
  11225. width = this.imageObj.width;
  11226. height = this.imageObj.height;
  11227. }
  11228. this.width = width;
  11229. this.height = height;
  11230. }
  11231. };
  11232. Node.prototype._drawImage = function (ctx) {
  11233. this._resizeImage(ctx);
  11234. this.left = this.x - this.width / 2;
  11235. this.top = this.y - this.height / 2;
  11236. var yLabel;
  11237. if (this.imageObj) {
  11238. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  11239. yLabel = this.y + this.height / 2;
  11240. }
  11241. else {
  11242. // image still loading... just draw the label for now
  11243. yLabel = this.y;
  11244. }
  11245. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  11246. };
  11247. Node.prototype._resizeBox = function (ctx) {
  11248. if (!this.width) {
  11249. var margin = 5;
  11250. var textSize = this.getTextSize(ctx);
  11251. this.width = textSize.width + 2 * margin;
  11252. this.height = textSize.height + 2 * margin;
  11253. }
  11254. };
  11255. Node.prototype._drawBox = function (ctx) {
  11256. this._resizeBox(ctx);
  11257. this.left = this.x - this.width / 2;
  11258. this.top = this.y - this.height / 2;
  11259. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  11260. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11261. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  11262. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  11263. ctx.fill();
  11264. ctx.stroke();
  11265. this._label(ctx, this.label, this.x, this.y);
  11266. };
  11267. Node.prototype._resizeDatabase = function (ctx) {
  11268. if (!this.width) {
  11269. var margin = 5;
  11270. var textSize = this.getTextSize(ctx);
  11271. var size = textSize.width + 2 * margin;
  11272. this.width = size;
  11273. this.height = size;
  11274. }
  11275. };
  11276. Node.prototype._drawDatabase = function (ctx) {
  11277. this._resizeDatabase(ctx);
  11278. this.left = this.x - this.width / 2;
  11279. this.top = this.y - this.height / 2;
  11280. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  11281. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11282. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  11283. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  11284. ctx.fill();
  11285. ctx.stroke();
  11286. this._label(ctx, this.label, this.x, this.y);
  11287. };
  11288. Node.prototype._resizeCircle = function (ctx) {
  11289. if (!this.width) {
  11290. var margin = 5;
  11291. var textSize = this.getTextSize(ctx);
  11292. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  11293. this.radius = diameter / 2;
  11294. this.width = diameter;
  11295. this.height = diameter;
  11296. }
  11297. };
  11298. Node.prototype._drawCircle = function (ctx) {
  11299. this._resizeCircle(ctx);
  11300. this.left = this.x - this.width / 2;
  11301. this.top = this.y - this.height / 2;
  11302. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  11303. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11304. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  11305. ctx.circle(this.x, this.y, this.radius);
  11306. ctx.fill();
  11307. ctx.stroke();
  11308. this._label(ctx, this.label, this.x, this.y);
  11309. };
  11310. Node.prototype._resizeEllipse = function (ctx) {
  11311. if (!this.width) {
  11312. var textSize = this.getTextSize(ctx);
  11313. this.width = textSize.width * 1.5;
  11314. this.height = textSize.height * 2;
  11315. if (this.width < this.height) {
  11316. this.width = this.height;
  11317. }
  11318. }
  11319. };
  11320. Node.prototype._drawEllipse = function (ctx) {
  11321. this._resizeEllipse(ctx);
  11322. this.left = this.x - this.width / 2;
  11323. this.top = this.y - this.height / 2;
  11324. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  11325. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11326. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  11327. ctx.ellipse(this.left, this.top, this.width, this.height);
  11328. ctx.fill();
  11329. ctx.stroke();
  11330. this._label(ctx, this.label, this.x, this.y);
  11331. };
  11332. Node.prototype._drawDot = function (ctx) {
  11333. this._drawShape(ctx, 'circle');
  11334. };
  11335. Node.prototype._drawTriangle = function (ctx) {
  11336. this._drawShape(ctx, 'triangle');
  11337. };
  11338. Node.prototype._drawTriangleDown = function (ctx) {
  11339. this._drawShape(ctx, 'triangleDown');
  11340. };
  11341. Node.prototype._drawSquare = function (ctx) {
  11342. this._drawShape(ctx, 'square');
  11343. };
  11344. Node.prototype._drawStar = function (ctx) {
  11345. this._drawShape(ctx, 'star');
  11346. };
  11347. Node.prototype._resizeShape = function (ctx) {
  11348. if (!this.width) {
  11349. var size = 2 * this.radius;
  11350. this.width = size;
  11351. this.height = size;
  11352. }
  11353. };
  11354. Node.prototype._drawShape = function (ctx, shape) {
  11355. this._resizeShape(ctx);
  11356. this.left = this.x - this.width / 2;
  11357. this.top = this.y - this.height / 2;
  11358. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  11359. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  11360. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  11361. ctx[shape](this.x, this.y, this.radius);
  11362. ctx.fill();
  11363. ctx.stroke();
  11364. if (this.label) {
  11365. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  11366. }
  11367. };
  11368. Node.prototype._resizeText = function (ctx) {
  11369. if (!this.width) {
  11370. var margin = 5;
  11371. var textSize = this.getTextSize(ctx);
  11372. this.width = textSize.width + 2 * margin;
  11373. this.height = textSize.height + 2 * margin;
  11374. }
  11375. };
  11376. Node.prototype._drawText = function (ctx) {
  11377. this._resizeText(ctx);
  11378. this.left = this.x - this.width / 2;
  11379. this.top = this.y - this.height / 2;
  11380. this._label(ctx, this.label, this.x, this.y);
  11381. };
  11382. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  11383. if (text) {
  11384. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11385. ctx.fillStyle = this.fontColor || "black";
  11386. ctx.textAlign = align || "center";
  11387. ctx.textBaseline = baseline || "middle";
  11388. var lines = text.split('\n'),
  11389. lineCount = lines.length,
  11390. fontSize = (this.fontSize + 4),
  11391. yLine = y + (1 - lineCount) / 2 * fontSize;
  11392. for (var i = 0; i < lineCount; i++) {
  11393. ctx.fillText(lines[i], x, yLine);
  11394. yLine += fontSize;
  11395. }
  11396. }
  11397. };
  11398. Node.prototype.getTextSize = function(ctx) {
  11399. if (this.label != undefined) {
  11400. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  11401. var lines = this.label.split('\n'),
  11402. height = (this.fontSize + 4) * lines.length,
  11403. width = 0;
  11404. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  11405. width = Math.max(width, ctx.measureText(lines[i]).width);
  11406. }
  11407. return {"width": width, "height": height};
  11408. }
  11409. else {
  11410. return {"width": 0, "height": 0};
  11411. }
  11412. };
  11413. /**
  11414. * @class Edge
  11415. *
  11416. * A edge connects two nodes
  11417. * @param {Object} properties Object with properties. Must contain
  11418. * At least properties from and to.
  11419. * Available properties: from (number),
  11420. * to (number), label (string, color (string),
  11421. * width (number), style (string),
  11422. * length (number), title (string)
  11423. * @param {Graph} graph A graph object, used to find and edge to
  11424. * nodes.
  11425. * @param {Object} constants An object with default values for
  11426. * example for the color
  11427. */
  11428. function Edge (properties, graph, constants) {
  11429. if (!graph) {
  11430. throw "No graph provided";
  11431. }
  11432. this.graph = graph;
  11433. // initialize constants
  11434. this.widthMin = constants.edges.widthMin;
  11435. this.widthMax = constants.edges.widthMax;
  11436. // initialize variables
  11437. this.id = undefined;
  11438. this.fromId = undefined;
  11439. this.toId = undefined;
  11440. this.style = constants.edges.style;
  11441. this.title = undefined;
  11442. this.width = constants.edges.width;
  11443. this.value = undefined;
  11444. this.length = constants.edges.length;
  11445. this.from = null; // a node
  11446. this.to = null; // a node
  11447. this.connected = false;
  11448. // Added to support dashed lines
  11449. // David Jordan
  11450. // 2012-08-08
  11451. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  11452. this.stiffness = undefined; // depends on the length of the edge
  11453. this.color = constants.edges.color;
  11454. this.widthFixed = false;
  11455. this.lengthFixed = false;
  11456. this.setProperties(properties, constants);
  11457. }
  11458. /**
  11459. * Set or overwrite properties for the edge
  11460. * @param {Object} properties an object with properties
  11461. * @param {Object} constants and object with default, global properties
  11462. */
  11463. Edge.prototype.setProperties = function(properties, constants) {
  11464. if (!properties) {
  11465. return;
  11466. }
  11467. if (properties.from != undefined) {this.fromId = properties.from;}
  11468. if (properties.to != undefined) {this.toId = properties.to;}
  11469. if (properties.id != undefined) {this.id = properties.id;}
  11470. if (properties.style != undefined) {this.style = properties.style;}
  11471. if (properties.label != undefined) {this.label = properties.label;}
  11472. if (this.label) {
  11473. this.fontSize = constants.edges.fontSize;
  11474. this.fontFace = constants.edges.fontFace;
  11475. this.fontColor = constants.edges.fontColor;
  11476. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  11477. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  11478. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  11479. }
  11480. if (properties.title != undefined) {this.title = properties.title;}
  11481. if (properties.width != undefined) {this.width = properties.width;}
  11482. if (properties.value != undefined) {this.value = properties.value;}
  11483. if (properties.length != undefined) {this.length = properties.length;}
  11484. // Added to support dashed lines
  11485. // David Jordan
  11486. // 2012-08-08
  11487. if (properties.dash) {
  11488. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  11489. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  11490. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  11491. }
  11492. if (properties.color != undefined) {this.color = properties.color;}
  11493. // A node is connected when it has a from and to node.
  11494. this.connect();
  11495. this.widthFixed = this.widthFixed || (properties.width != undefined);
  11496. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  11497. this.stiffness = 1 / this.length;
  11498. // set draw method based on style
  11499. switch (this.style) {
  11500. case 'line': this.draw = this._drawLine; break;
  11501. case 'arrow': this.draw = this._drawArrow; break;
  11502. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  11503. case 'dash-line': this.draw = this._drawDashLine; break;
  11504. default: this.draw = this._drawLine; break;
  11505. }
  11506. };
  11507. /**
  11508. * Connect an edge to its nodes
  11509. */
  11510. Edge.prototype.connect = function () {
  11511. this.disconnect();
  11512. this.from = this.graph.nodes[this.fromId] || null;
  11513. this.to = this.graph.nodes[this.toId] || null;
  11514. this.connected = (this.from && this.to);
  11515. if (this.connected) {
  11516. this.from.attachEdge(this);
  11517. this.to.attachEdge(this);
  11518. }
  11519. else {
  11520. if (this.from) {
  11521. this.from.detachEdge(this);
  11522. }
  11523. if (this.to) {
  11524. this.to.detachEdge(this);
  11525. }
  11526. }
  11527. };
  11528. /**
  11529. * Disconnect an edge from its nodes
  11530. */
  11531. Edge.prototype.disconnect = function () {
  11532. if (this.from) {
  11533. this.from.detachEdge(this);
  11534. this.from = null;
  11535. }
  11536. if (this.to) {
  11537. this.to.detachEdge(this);
  11538. this.to = null;
  11539. }
  11540. this.connected = false;
  11541. };
  11542. /**
  11543. * get the title of this edge.
  11544. * @return {string} title The title of the edge, or undefined when no title
  11545. * has been set.
  11546. */
  11547. Edge.prototype.getTitle = function() {
  11548. return this.title;
  11549. };
  11550. /**
  11551. * Retrieve the value of the edge. Can be undefined
  11552. * @return {Number} value
  11553. */
  11554. Edge.prototype.getValue = function() {
  11555. return this.value;
  11556. };
  11557. /**
  11558. * Adjust the value range of the edge. The edge will adjust it's width
  11559. * based on its value.
  11560. * @param {Number} min
  11561. * @param {Number} max
  11562. */
  11563. Edge.prototype.setValueRange = function(min, max) {
  11564. if (!this.widthFixed && this.value !== undefined) {
  11565. var factor = (this.widthMax - this.widthMin) / (max - min);
  11566. this.width = (this.value - min) * factor + this.widthMin;
  11567. }
  11568. };
  11569. /**
  11570. * Redraw a edge
  11571. * Draw this edge in the given canvas
  11572. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11573. * @param {CanvasRenderingContext2D} ctx
  11574. */
  11575. Edge.prototype.draw = function(ctx) {
  11576. throw "Method draw not initialized in edge";
  11577. };
  11578. /**
  11579. * Check if this object is overlapping with the provided object
  11580. * @param {Object} obj an object with parameters left, top
  11581. * @return {boolean} True if location is located on the edge
  11582. */
  11583. Edge.prototype.isOverlappingWith = function(obj) {
  11584. var distMax = 10;
  11585. var xFrom = this.from.x;
  11586. var yFrom = this.from.y;
  11587. var xTo = this.to.x;
  11588. var yTo = this.to.y;
  11589. var xObj = obj.left;
  11590. var yObj = obj.top;
  11591. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  11592. return (dist < distMax);
  11593. };
  11594. /**
  11595. * Redraw a edge as a line
  11596. * Draw this edge in the given canvas
  11597. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11598. * @param {CanvasRenderingContext2D} ctx
  11599. * @private
  11600. */
  11601. Edge.prototype._drawLine = function(ctx) {
  11602. // set style
  11603. ctx.strokeStyle = this.color;
  11604. ctx.lineWidth = this._getLineWidth();
  11605. var point;
  11606. if (this.from != this.to) {
  11607. // draw line
  11608. this._line(ctx);
  11609. // draw label
  11610. if (this.label) {
  11611. point = this._pointOnLine(0.5);
  11612. this._label(ctx, this.label, point.x, point.y);
  11613. }
  11614. }
  11615. else {
  11616. var x, y;
  11617. var radius = this.length / 4;
  11618. var node = this.from;
  11619. if (!node.width) {
  11620. node.resize(ctx);
  11621. }
  11622. if (node.width > node.height) {
  11623. x = node.x + node.width / 2;
  11624. y = node.y - radius;
  11625. }
  11626. else {
  11627. x = node.x + radius;
  11628. y = node.y - node.height / 2;
  11629. }
  11630. this._circle(ctx, x, y, radius);
  11631. point = this._pointOnCircle(x, y, radius, 0.5);
  11632. this._label(ctx, this.label, point.x, point.y);
  11633. }
  11634. };
  11635. /**
  11636. * Get the line width of the edge. Depends on width and whether one of the
  11637. * connected nodes is selected.
  11638. * @return {Number} width
  11639. * @private
  11640. */
  11641. Edge.prototype._getLineWidth = function() {
  11642. if (this.from.selected || this.to.selected) {
  11643. return Math.min(this.width * 2, this.widthMax);
  11644. }
  11645. else {
  11646. return this.width;
  11647. }
  11648. };
  11649. /**
  11650. * Draw a line between two nodes
  11651. * @param {CanvasRenderingContext2D} ctx
  11652. * @private
  11653. */
  11654. Edge.prototype._line = function (ctx) {
  11655. // draw a straight line
  11656. ctx.beginPath();
  11657. ctx.moveTo(this.from.x, this.from.y);
  11658. ctx.lineTo(this.to.x, this.to.y);
  11659. ctx.stroke();
  11660. };
  11661. /**
  11662. * Draw a line from a node to itself, a circle
  11663. * @param {CanvasRenderingContext2D} ctx
  11664. * @param {Number} x
  11665. * @param {Number} y
  11666. * @param {Number} radius
  11667. * @private
  11668. */
  11669. Edge.prototype._circle = function (ctx, x, y, radius) {
  11670. // draw a circle
  11671. ctx.beginPath();
  11672. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  11673. ctx.stroke();
  11674. };
  11675. /**
  11676. * Draw label with white background and with the middle at (x, y)
  11677. * @param {CanvasRenderingContext2D} ctx
  11678. * @param {String} text
  11679. * @param {Number} x
  11680. * @param {Number} y
  11681. * @private
  11682. */
  11683. Edge.prototype._label = function (ctx, text, x, y) {
  11684. if (text) {
  11685. // TODO: cache the calculated size
  11686. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  11687. this.fontSize + "px " + this.fontFace;
  11688. ctx.fillStyle = 'white';
  11689. var width = ctx.measureText(text).width;
  11690. var height = this.fontSize;
  11691. var left = x - width / 2;
  11692. var top = y - height / 2;
  11693. ctx.fillRect(left, top, width, height);
  11694. // draw text
  11695. ctx.fillStyle = this.fontColor || "black";
  11696. ctx.textAlign = "left";
  11697. ctx.textBaseline = "top";
  11698. ctx.fillText(text, left, top);
  11699. }
  11700. };
  11701. /**
  11702. * Redraw a edge as a dashed line
  11703. * Draw this edge in the given canvas
  11704. * @author David Jordan
  11705. * @date 2012-08-08
  11706. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11707. * @param {CanvasRenderingContext2D} ctx
  11708. * @private
  11709. */
  11710. Edge.prototype._drawDashLine = function(ctx) {
  11711. // set style
  11712. ctx.strokeStyle = this.color;
  11713. ctx.lineWidth = this._getLineWidth();
  11714. // draw dashed line
  11715. ctx.beginPath();
  11716. ctx.lineCap = 'round';
  11717. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  11718. {
  11719. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11720. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  11721. }
  11722. else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
  11723. {
  11724. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  11725. [this.dash.length,this.dash.gap]);
  11726. }
  11727. else //If all else fails draw a line
  11728. {
  11729. ctx.moveTo(this.from.x, this.from.y);
  11730. ctx.lineTo(this.to.x, this.to.y);
  11731. }
  11732. ctx.stroke();
  11733. // draw label
  11734. if (this.label) {
  11735. var point = this._pointOnLine(0.5);
  11736. this._label(ctx, this.label, point.x, point.y);
  11737. }
  11738. };
  11739. /**
  11740. * Get a point on a line
  11741. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  11742. * @return {Object} point
  11743. * @private
  11744. */
  11745. Edge.prototype._pointOnLine = function (percentage) {
  11746. return {
  11747. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  11748. y: (1 - percentage) * this.from.y + percentage * this.to.y
  11749. }
  11750. };
  11751. /**
  11752. * Get a point on a circle
  11753. * @param {Number} x
  11754. * @param {Number} y
  11755. * @param {Number} radius
  11756. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  11757. * @return {Object} point
  11758. * @private
  11759. */
  11760. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  11761. var angle = (percentage - 3/8) * 2 * Math.PI;
  11762. return {
  11763. x: x + radius * Math.cos(angle),
  11764. y: y - radius * Math.sin(angle)
  11765. }
  11766. };
  11767. /**
  11768. * Redraw a edge as a line with an arrow halfway the line
  11769. * Draw this edge in the given canvas
  11770. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11771. * @param {CanvasRenderingContext2D} ctx
  11772. * @private
  11773. */
  11774. Edge.prototype._drawArrowCenter = function(ctx) {
  11775. var point;
  11776. // set style
  11777. ctx.strokeStyle = this.color;
  11778. ctx.fillStyle = this.color;
  11779. ctx.lineWidth = this._getLineWidth();
  11780. if (this.from != this.to) {
  11781. // draw line
  11782. this._line(ctx);
  11783. // draw an arrow halfway the line
  11784. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  11785. var length = 10 + 5 * this.width; // TODO: make customizable?
  11786. point = this._pointOnLine(0.5);
  11787. ctx.arrow(point.x, point.y, angle, length);
  11788. ctx.fill();
  11789. ctx.stroke();
  11790. // draw label
  11791. if (this.label) {
  11792. point = this._pointOnLine(0.5);
  11793. this._label(ctx, this.label, point.x, point.y);
  11794. }
  11795. }
  11796. else {
  11797. // draw circle
  11798. var x, y;
  11799. var radius = this.length / 4;
  11800. var node = this.from;
  11801. if (!node.width) {
  11802. node.resize(ctx);
  11803. }
  11804. if (node.width > node.height) {
  11805. x = node.x + node.width / 2;
  11806. y = node.y - radius;
  11807. }
  11808. else {
  11809. x = node.x + radius;
  11810. y = node.y - node.height / 2;
  11811. }
  11812. this._circle(ctx, x, y, radius);
  11813. // draw all arrows
  11814. var angle = 0.2 * Math.PI;
  11815. var length = 10 + 5 * this.width; // TODO: make customizable?
  11816. point = this._pointOnCircle(x, y, radius, 0.5);
  11817. ctx.arrow(point.x, point.y, angle, length);
  11818. ctx.fill();
  11819. ctx.stroke();
  11820. // draw label
  11821. if (this.label) {
  11822. point = this._pointOnCircle(x, y, radius, 0.5);
  11823. this._label(ctx, this.label, point.x, point.y);
  11824. }
  11825. }
  11826. };
  11827. /**
  11828. * Redraw a edge as a line with an arrow
  11829. * Draw this edge in the given canvas
  11830. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11831. * @param {CanvasRenderingContext2D} ctx
  11832. * @private
  11833. */
  11834. Edge.prototype._drawArrow = function(ctx) {
  11835. // set style
  11836. ctx.strokeStyle = this.color;
  11837. ctx.fillStyle = this.color;
  11838. ctx.lineWidth = this._getLineWidth();
  11839. // draw line
  11840. var angle, length;
  11841. if (this.from != this.to) {
  11842. // calculate length and angle of the line
  11843. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  11844. var dx = (this.to.x - this.from.x);
  11845. var dy = (this.to.y - this.from.y);
  11846. var lEdge = Math.sqrt(dx * dx + dy * dy);
  11847. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  11848. var pFrom = (lEdge - lFrom) / lEdge;
  11849. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  11850. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  11851. var lTo = this.to.distanceToBorder(ctx, angle);
  11852. var pTo = (lEdge - lTo) / lEdge;
  11853. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  11854. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  11855. ctx.beginPath();
  11856. ctx.moveTo(xFrom, yFrom);
  11857. ctx.lineTo(xTo, yTo);
  11858. ctx.stroke();
  11859. // draw arrow at the end of the line
  11860. length = 10 + 5 * this.width; // TODO: make customizable?
  11861. ctx.arrow(xTo, yTo, angle, length);
  11862. ctx.fill();
  11863. ctx.stroke();
  11864. // draw label
  11865. if (this.label) {
  11866. var point = this._pointOnLine(0.5);
  11867. this._label(ctx, this.label, point.x, point.y);
  11868. }
  11869. }
  11870. else {
  11871. // draw circle
  11872. var node = this.from;
  11873. var x, y, arrow;
  11874. var radius = this.length / 4;
  11875. if (!node.width) {
  11876. node.resize(ctx);
  11877. }
  11878. if (node.width > node.height) {
  11879. x = node.x + node.width / 2;
  11880. y = node.y - radius;
  11881. arrow = {
  11882. x: x,
  11883. y: node.y,
  11884. angle: 0.9 * Math.PI
  11885. };
  11886. }
  11887. else {
  11888. x = node.x + radius;
  11889. y = node.y - node.height / 2;
  11890. arrow = {
  11891. x: node.x,
  11892. y: y,
  11893. angle: 0.6 * Math.PI
  11894. };
  11895. }
  11896. ctx.beginPath();
  11897. // TODO: do not draw a circle, but an arc
  11898. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  11899. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  11900. ctx.stroke();
  11901. // draw all arrows
  11902. length = 10 + 5 * this.width; // TODO: make customizable?
  11903. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  11904. ctx.fill();
  11905. ctx.stroke();
  11906. // draw label
  11907. if (this.label) {
  11908. point = this._pointOnCircle(x, y, radius, 0.5);
  11909. this._label(ctx, this.label, point.x, point.y);
  11910. }
  11911. }
  11912. };
  11913. /**
  11914. * Calculate the distance between a point (x3,y3) and a line segment from
  11915. * (x1,y1) to (x2,y2).
  11916. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  11917. * @param {number} x1
  11918. * @param {number} y1
  11919. * @param {number} x2
  11920. * @param {number} y2
  11921. * @param {number} x3
  11922. * @param {number} y3
  11923. * @private
  11924. */
  11925. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  11926. var px = x2-x1,
  11927. py = y2-y1,
  11928. something = px*px + py*py,
  11929. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  11930. if (u > 1) {
  11931. u = 1;
  11932. }
  11933. else if (u < 0) {
  11934. u = 0;
  11935. }
  11936. var x = x1 + u * px,
  11937. y = y1 + u * py,
  11938. dx = x - x3,
  11939. dy = y - y3;
  11940. //# Note: If the actual distance does not matter,
  11941. //# if you only want to compare what this function
  11942. //# returns to other results of this function, you
  11943. //# can just return the squared distance instead
  11944. //# (i.e. remove the sqrt) to gain a little performance
  11945. return Math.sqrt(dx*dx + dy*dy);
  11946. };
  11947. /**
  11948. * Popup is a class to create a popup window with some text
  11949. * @param {Element} container The container object.
  11950. * @param {Number} [x]
  11951. * @param {Number} [y]
  11952. * @param {String} [text]
  11953. */
  11954. function Popup(container, x, y, text) {
  11955. if (container) {
  11956. this.container = container;
  11957. }
  11958. else {
  11959. this.container = document.body;
  11960. }
  11961. this.x = 0;
  11962. this.y = 0;
  11963. this.padding = 5;
  11964. if (x !== undefined && y !== undefined ) {
  11965. this.setPosition(x, y);
  11966. }
  11967. if (text !== undefined) {
  11968. this.setText(text);
  11969. }
  11970. // create the frame
  11971. this.frame = document.createElement("div");
  11972. var style = this.frame.style;
  11973. style.position = "absolute";
  11974. style.visibility = "hidden";
  11975. style.border = "1px solid #666";
  11976. style.color = "black";
  11977. style.padding = this.padding + "px";
  11978. style.backgroundColor = "#FFFFC6";
  11979. style.borderRadius = "3px";
  11980. style.MozBorderRadius = "3px";
  11981. style.WebkitBorderRadius = "3px";
  11982. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  11983. style.whiteSpace = "nowrap";
  11984. this.container.appendChild(this.frame);
  11985. };
  11986. /**
  11987. * @param {number} x Horizontal position of the popup window
  11988. * @param {number} y Vertical position of the popup window
  11989. */
  11990. Popup.prototype.setPosition = function(x, y) {
  11991. this.x = parseInt(x);
  11992. this.y = parseInt(y);
  11993. };
  11994. /**
  11995. * Set the text for the popup window. This can be HTML code
  11996. * @param {string} text
  11997. */
  11998. Popup.prototype.setText = function(text) {
  11999. this.frame.innerHTML = text;
  12000. };
  12001. /**
  12002. * Show the popup window
  12003. * @param {boolean} show Optional. Show or hide the window
  12004. */
  12005. Popup.prototype.show = function (show) {
  12006. if (show === undefined) {
  12007. show = true;
  12008. }
  12009. if (show) {
  12010. var height = this.frame.clientHeight;
  12011. var width = this.frame.clientWidth;
  12012. var maxHeight = this.frame.parentNode.clientHeight;
  12013. var maxWidth = this.frame.parentNode.clientWidth;
  12014. var top = (this.y - height);
  12015. if (top + height + this.padding > maxHeight) {
  12016. top = maxHeight - height - this.padding;
  12017. }
  12018. if (top < this.padding) {
  12019. top = this.padding;
  12020. }
  12021. var left = this.x;
  12022. if (left + width + this.padding > maxWidth) {
  12023. left = maxWidth - width - this.padding;
  12024. }
  12025. if (left < this.padding) {
  12026. left = this.padding;
  12027. }
  12028. this.frame.style.left = left + "px";
  12029. this.frame.style.top = top + "px";
  12030. this.frame.style.visibility = "visible";
  12031. }
  12032. else {
  12033. this.hide();
  12034. }
  12035. };
  12036. /**
  12037. * Hide the popup window
  12038. */
  12039. Popup.prototype.hide = function () {
  12040. this.frame.style.visibility = "hidden";
  12041. };
  12042. /**
  12043. * @class Groups
  12044. * This class can store groups and properties specific for groups.
  12045. */
  12046. Groups = function () {
  12047. this.clear();
  12048. this.defaultIndex = 0;
  12049. };
  12050. /**
  12051. * default constants for group colors
  12052. */
  12053. Groups.DEFAULT = [
  12054. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  12055. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  12056. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  12057. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  12058. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  12059. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  12060. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  12061. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  12062. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  12063. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  12064. ];
  12065. /**
  12066. * Clear all groups
  12067. */
  12068. Groups.prototype.clear = function () {
  12069. this.groups = {};
  12070. this.groups.length = function()
  12071. {
  12072. var i = 0;
  12073. for ( var p in this ) {
  12074. if (this.hasOwnProperty(p)) {
  12075. i++;
  12076. }
  12077. }
  12078. return i;
  12079. }
  12080. };
  12081. /**
  12082. * get group properties of a groupname. If groupname is not found, a new group
  12083. * is added.
  12084. * @param {*} groupname Can be a number, string, Date, etc.
  12085. * @return {Object} group The created group, containing all group properties
  12086. */
  12087. Groups.prototype.get = function (groupname) {
  12088. var group = this.groups[groupname];
  12089. if (group == undefined) {
  12090. // create new group
  12091. var index = this.defaultIndex % Groups.DEFAULT.length;
  12092. this.defaultIndex++;
  12093. group = {};
  12094. group.color = Groups.DEFAULT[index];
  12095. this.groups[groupname] = group;
  12096. }
  12097. return group;
  12098. };
  12099. /**
  12100. * Add a custom group style
  12101. * @param {String} groupname
  12102. * @param {Object} style An object containing borderColor,
  12103. * backgroundColor, etc.
  12104. * @return {Object} group The created group object
  12105. */
  12106. Groups.prototype.add = function (groupname, style) {
  12107. this.groups[groupname] = style;
  12108. if (style.color) {
  12109. style.color = Node.parseColor(style.color);
  12110. }
  12111. return style;
  12112. };
  12113. /**
  12114. * @class Images
  12115. * This class loads images and keeps them stored.
  12116. */
  12117. Images = function () {
  12118. this.images = {};
  12119. this.callback = undefined;
  12120. };
  12121. /**
  12122. * Set an onload callback function. This will be called each time an image
  12123. * is loaded
  12124. * @param {function} callback
  12125. */
  12126. Images.prototype.setOnloadCallback = function(callback) {
  12127. this.callback = callback;
  12128. };
  12129. /**
  12130. *
  12131. * @param {string} url Url of the image
  12132. * @return {Image} img The image object
  12133. */
  12134. Images.prototype.load = function(url) {
  12135. var img = this.images[url];
  12136. if (img == undefined) {
  12137. // create the image
  12138. var images = this;
  12139. img = new Image();
  12140. this.images[url] = img;
  12141. img.onload = function() {
  12142. if (images.callback) {
  12143. images.callback(this);
  12144. }
  12145. };
  12146. img.src = url;
  12147. }
  12148. return img;
  12149. };
  12150. /**
  12151. * @constructor Graph
  12152. * Create a graph visualization, displaying nodes and edges.
  12153. *
  12154. * @param {Element} container The DOM element in which the Graph will
  12155. * be created. Normally a div element.
  12156. * @param {Object} data An object containing parameters
  12157. * {Array} nodes
  12158. * {Array} edges
  12159. * @param {Object} options Options
  12160. */
  12161. function Graph (container, data, options) {
  12162. // create variables and set default values
  12163. this.containerElement = container;
  12164. this.width = '100%';
  12165. this.height = '100%';
  12166. this.refreshRate = 50; // milliseconds
  12167. this.stabilize = true; // stabilize before displaying the graph
  12168. this.selectable = true;
  12169. // set constant values
  12170. this.constants = {
  12171. nodes: {
  12172. radiusMin: 5,
  12173. radiusMax: 20,
  12174. radius: 5,
  12175. distance: 100, // px
  12176. shape: 'ellipse',
  12177. image: undefined,
  12178. widthMin: 16, // px
  12179. widthMax: 64, // px
  12180. fontColor: 'black',
  12181. fontSize: 14, // px
  12182. //fontFace: verdana,
  12183. fontFace: 'arial',
  12184. color: {
  12185. border: '#2B7CE9',
  12186. background: '#97C2FC',
  12187. highlight: {
  12188. border: '#2B7CE9',
  12189. background: '#D2E5FF'
  12190. }
  12191. },
  12192. borderColor: '#2B7CE9',
  12193. backgroundColor: '#97C2FC',
  12194. highlightColor: '#D2E5FF',
  12195. group: undefined
  12196. },
  12197. edges: {
  12198. widthMin: 1,
  12199. widthMax: 15,
  12200. width: 1,
  12201. style: 'line',
  12202. color: '#343434',
  12203. fontColor: '#343434',
  12204. fontSize: 14, // px
  12205. fontFace: 'arial',
  12206. //distance: 100, //px
  12207. length: 100, // px
  12208. dash: {
  12209. length: 10,
  12210. gap: 5,
  12211. altLength: undefined
  12212. }
  12213. },
  12214. minForce: 0.05,
  12215. minVelocity: 0.02, // px/s
  12216. maxIterations: 1000 // maximum number of iteration to stabilize
  12217. };
  12218. var graph = this;
  12219. this.nodes = {}; // object with Node objects
  12220. this.edges = {}; // object with Edge objects
  12221. // TODO: create a counter to keep track on the number of nodes having values
  12222. // TODO: create a counter to keep track on the number of nodes currently moving
  12223. // TODO: create a counter to keep track on the number of edges having values
  12224. this.nodesData = null; // A DataSet or DataView
  12225. this.edgesData = null; // A DataSet or DataView
  12226. // create event listeners used to subscribe on the DataSets of the nodes and edges
  12227. var me = this;
  12228. this.nodesListeners = {
  12229. 'add': function (event, params) {
  12230. me._addNodes(params.items);
  12231. me.start();
  12232. },
  12233. 'update': function (event, params) {
  12234. me._updateNodes(params.items);
  12235. me.start();
  12236. },
  12237. 'remove': function (event, params) {
  12238. me._removeNodes(params.items);
  12239. me.start();
  12240. }
  12241. };
  12242. this.edgesListeners = {
  12243. 'add': function (event, params) {
  12244. me._addEdges(params.items);
  12245. me.start();
  12246. },
  12247. 'update': function (event, params) {
  12248. me._updateEdges(params.items);
  12249. me.start();
  12250. },
  12251. 'remove': function (event, params) {
  12252. me._removeEdges(params.items);
  12253. me.start();
  12254. }
  12255. };
  12256. this.groups = new Groups(); // object with groups
  12257. this.images = new Images(); // object with images
  12258. this.images.setOnloadCallback(function () {
  12259. graph._redraw();
  12260. });
  12261. // properties of the data
  12262. this.moving = false; // True if any of the nodes have an undefined position
  12263. this.selection = [];
  12264. this.timer = undefined;
  12265. // create a frame and canvas
  12266. this._create();
  12267. // apply options
  12268. this.setOptions(options);
  12269. // draw data
  12270. this.setData(data);
  12271. }
  12272. /**
  12273. * Set nodes and edges, and optionally options as well.
  12274. *
  12275. * @param {Object} data Object containing parameters:
  12276. * {Array | DataSet | DataView} [nodes] Array with nodes
  12277. * {Array | DataSet | DataView} [edges] Array with edges
  12278. * {String} [dot] String containing data in DOT format
  12279. * {Options} [options] Object with options
  12280. */
  12281. Graph.prototype.setData = function(data) {
  12282. if (data && data.dot && (data.nodes || data.edges)) {
  12283. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  12284. ' parameter pair "nodes" and "edges", but not both.');
  12285. }
  12286. // set options
  12287. this.setOptions(data && data.options);
  12288. // set all data
  12289. if (data && data.dot) {
  12290. // parse DOT file
  12291. if(data && data.dot) {
  12292. var dotData = vis.util.DOTToGraph(data.dot);
  12293. this.setData(dotData);
  12294. return;
  12295. }
  12296. }
  12297. else {
  12298. this._setNodes(data && data.nodes);
  12299. this._setEdges(data && data.edges);
  12300. }
  12301. // find a stable position or start animating to a stable position
  12302. if (this.stabilize) {
  12303. this._doStabilize();
  12304. }
  12305. this.start();
  12306. };
  12307. /**
  12308. * Set options
  12309. * @param {Object} options
  12310. */
  12311. Graph.prototype.setOptions = function (options) {
  12312. if (options) {
  12313. // retrieve parameter values
  12314. if (options.width != undefined) {this.width = options.width;}
  12315. if (options.height != undefined) {this.height = options.height;}
  12316. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  12317. if (options.selectable != undefined) {this.selectable = options.selectable;}
  12318. // TODO: work out these options and document them
  12319. if (options.edges) {
  12320. for (var prop in options.edges) {
  12321. if (options.edges.hasOwnProperty(prop)) {
  12322. this.constants.edges[prop] = options.edges[prop];
  12323. }
  12324. }
  12325. if (options.edges.length != undefined &&
  12326. options.nodes && options.nodes.distance == undefined) {
  12327. this.constants.edges.length = options.edges.length;
  12328. this.constants.nodes.distance = options.edges.length * 1.25;
  12329. }
  12330. if (!options.edges.fontColor) {
  12331. this.constants.edges.fontColor = options.edges.color;
  12332. }
  12333. // Added to support dashed lines
  12334. // David Jordan
  12335. // 2012-08-08
  12336. if (options.edges.dash) {
  12337. if (options.edges.dash.length != undefined) {
  12338. this.constants.edges.dash.length = options.edges.dash.length;
  12339. }
  12340. if (options.edges.dash.gap != undefined) {
  12341. this.constants.edges.dash.gap = options.edges.dash.gap;
  12342. }
  12343. if (options.edges.dash.altLength != undefined) {
  12344. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  12345. }
  12346. }
  12347. }
  12348. if (options.nodes) {
  12349. for (prop in options.nodes) {
  12350. if (options.nodes.hasOwnProperty(prop)) {
  12351. this.constants.nodes[prop] = options.nodes[prop];
  12352. }
  12353. }
  12354. if (options.nodes.color) {
  12355. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  12356. }
  12357. /*
  12358. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  12359. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  12360. */
  12361. }
  12362. if (options.groups) {
  12363. for (var groupname in options.groups) {
  12364. if (options.groups.hasOwnProperty(groupname)) {
  12365. var group = options.groups[groupname];
  12366. this.groups.add(groupname, group);
  12367. }
  12368. }
  12369. }
  12370. }
  12371. this.setSize(this.width, this.height);
  12372. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  12373. this._setScale(1);
  12374. };
  12375. /**
  12376. * fire an event
  12377. * @param {String} event The name of an event, for example 'select'
  12378. * @param {Object} params Optional object with event parameters
  12379. * @private
  12380. */
  12381. Graph.prototype._trigger = function (event, params) {
  12382. events.trigger(this, event, params);
  12383. };
  12384. /**
  12385. * Create the main frame for the Graph.
  12386. * This function is executed once when a Graph object is created. The frame
  12387. * contains a canvas, and this canvas contains all objects like the axis and
  12388. * nodes.
  12389. * @private
  12390. */
  12391. Graph.prototype._create = function () {
  12392. // remove all elements from the container element.
  12393. while (this.containerElement.hasChildNodes()) {
  12394. this.containerElement.removeChild(this.containerElement.firstChild);
  12395. }
  12396. this.frame = document.createElement('div');
  12397. this.frame.className = 'graph-frame';
  12398. this.frame.style.position = 'relative';
  12399. this.frame.style.overflow = 'hidden';
  12400. // create the graph canvas (HTML canvas element)
  12401. this.frame.canvas = document.createElement( 'canvas' );
  12402. this.frame.canvas.style.position = 'relative';
  12403. this.frame.appendChild(this.frame.canvas);
  12404. if (!this.frame.canvas.getContext) {
  12405. var noCanvas = document.createElement( 'DIV' );
  12406. noCanvas.style.color = 'red';
  12407. noCanvas.style.fontWeight = 'bold' ;
  12408. noCanvas.style.padding = '10px';
  12409. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  12410. this.frame.canvas.appendChild(noCanvas);
  12411. }
  12412. var me = this;
  12413. this.drag = {};
  12414. this.pinch = {};
  12415. this.hammer = Hammer(this.frame.canvas, {
  12416. prevent_default: true
  12417. });
  12418. this.hammer.on('tap', me._onTap.bind(me) );
  12419. this.hammer.on('hold', me._onHold.bind(me) );
  12420. this.hammer.on('pinch', me._onPinch.bind(me) );
  12421. this.hammer.on('touch', me._onTouch.bind(me) );
  12422. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  12423. this.hammer.on('drag', me._onDrag.bind(me) );
  12424. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  12425. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  12426. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  12427. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  12428. // add the frame to the container element
  12429. this.containerElement.appendChild(this.frame);
  12430. };
  12431. /**
  12432. *
  12433. * @param {{x: Number, y: Number}} pointer
  12434. * @return {Number | null} node
  12435. * @private
  12436. */
  12437. Graph.prototype._getNodeAt = function (pointer) {
  12438. var x = this._canvasToX(pointer.x);
  12439. var y = this._canvasToY(pointer.y);
  12440. var obj = {
  12441. left: x,
  12442. top: y,
  12443. right: x,
  12444. bottom: y
  12445. };
  12446. // if there are overlapping nodes, select the last one, this is the
  12447. // one which is drawn on top of the others
  12448. var overlappingNodes = this._getNodesOverlappingWith(obj);
  12449. return (overlappingNodes.length > 0) ?
  12450. overlappingNodes[overlappingNodes.length - 1] : null;
  12451. };
  12452. /**
  12453. * Get the pointer location from a touch location
  12454. * @param {{pageX: Number, pageY: Number}} touch
  12455. * @return {{x: Number, y: Number}} pointer
  12456. * @private
  12457. */
  12458. Graph.prototype._getPointer = function (touch) {
  12459. return {
  12460. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  12461. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  12462. };
  12463. };
  12464. /**
  12465. * On start of a touch gesture, store the pointer
  12466. * @param event
  12467. * @private
  12468. */
  12469. Graph.prototype._onTouch = function (event) {
  12470. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  12471. this.drag.pinched = false;
  12472. this.pinch.scale = this._getScale();
  12473. };
  12474. /**
  12475. * handle drag start event
  12476. * @private
  12477. */
  12478. Graph.prototype._onDragStart = function () {
  12479. var drag = this.drag;
  12480. drag.selection = [];
  12481. drag.translation = this._getTranslation();
  12482. drag.nodeId = this._getNodeAt(drag.pointer);
  12483. // note: drag.pointer is set in _onTouch to get the initial touch location
  12484. var node = this.nodes[drag.nodeId];
  12485. if (node) {
  12486. // select the clicked node if not yet selected
  12487. if (!node.isSelected()) {
  12488. this._selectNodes([drag.nodeId]);
  12489. }
  12490. // create an array with the selected nodes and their original location and status
  12491. var me = this;
  12492. this.selection.forEach(function (id) {
  12493. var node = me.nodes[id];
  12494. if (node) {
  12495. var s = {
  12496. id: id,
  12497. node: node,
  12498. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  12499. x: node.x,
  12500. y: node.y,
  12501. xFixed: node.xFixed,
  12502. yFixed: node.yFixed
  12503. };
  12504. node.xFixed = true;
  12505. node.yFixed = true;
  12506. drag.selection.push(s);
  12507. }
  12508. });
  12509. }
  12510. };
  12511. /**
  12512. * handle drag event
  12513. * @private
  12514. */
  12515. Graph.prototype._onDrag = function (event) {
  12516. if (this.drag.pinched) {
  12517. return;
  12518. }
  12519. var pointer = this._getPointer(event.gesture.touches[0]);
  12520. var me = this,
  12521. drag = this.drag,
  12522. selection = drag.selection;
  12523. if (selection && selection.length) {
  12524. // calculate delta's and new location
  12525. var deltaX = pointer.x - drag.pointer.x,
  12526. deltaY = pointer.y - drag.pointer.y;
  12527. // update position of all selected nodes
  12528. selection.forEach(function (s) {
  12529. var node = s.node;
  12530. if (!s.xFixed) {
  12531. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  12532. }
  12533. if (!s.yFixed) {
  12534. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  12535. }
  12536. });
  12537. // start animation if not yet running
  12538. if (!this.moving) {
  12539. this.moving = true;
  12540. this.start();
  12541. }
  12542. }
  12543. else {
  12544. // move the graph
  12545. var diffX = pointer.x - this.drag.pointer.x;
  12546. var diffY = pointer.y - this.drag.pointer.y;
  12547. this._setTranslation(
  12548. this.drag.translation.x + diffX,
  12549. this.drag.translation.y + diffY);
  12550. this._redraw();
  12551. this.moved = true;
  12552. }
  12553. };
  12554. /**
  12555. * handle drag start event
  12556. * @private
  12557. */
  12558. Graph.prototype._onDragEnd = function () {
  12559. var selection = this.drag.selection;
  12560. if (selection) {
  12561. selection.forEach(function (s) {
  12562. // restore original xFixed and yFixed
  12563. s.node.xFixed = s.xFixed;
  12564. s.node.yFixed = s.yFixed;
  12565. });
  12566. }
  12567. };
  12568. /**
  12569. * handle tap/click event: select/unselect a node
  12570. * @private
  12571. */
  12572. Graph.prototype._onTap = function (event) {
  12573. var pointer = this._getPointer(event.gesture.touches[0]);
  12574. var nodeId = this._getNodeAt(pointer);
  12575. var node = this.nodes[nodeId];
  12576. if (node) {
  12577. // select this node
  12578. this._selectNodes([nodeId]);
  12579. if (!this.moving) {
  12580. this._redraw();
  12581. }
  12582. }
  12583. else {
  12584. // remove selection
  12585. this._unselectNodes();
  12586. this._redraw();
  12587. }
  12588. };
  12589. /**
  12590. * handle long tap event: multi select nodes
  12591. * @private
  12592. */
  12593. Graph.prototype._onHold = function (event) {
  12594. var pointer = this._getPointer(event.gesture.touches[0]);
  12595. var nodeId = this._getNodeAt(pointer);
  12596. var node = this.nodes[nodeId];
  12597. if (node) {
  12598. if (!node.isSelected()) {
  12599. // select this node, keep previous selection
  12600. var append = true;
  12601. this._selectNodes([nodeId], append);
  12602. }
  12603. else {
  12604. this._unselectNodes([nodeId]);
  12605. }
  12606. if (!this.moving) {
  12607. this._redraw();
  12608. }
  12609. }
  12610. else {
  12611. // Do nothing
  12612. }
  12613. };
  12614. /**
  12615. * Handle pinch event
  12616. * @param event
  12617. * @private
  12618. */
  12619. Graph.prototype._onPinch = function (event) {
  12620. var pointer = this._getPointer(event.gesture.center);
  12621. this.drag.pinched = true;
  12622. if (!('scale' in this.pinch)) {
  12623. this.pinch.scale = 1;
  12624. }
  12625. // TODO: enable moving while pinching?
  12626. var scale = this.pinch.scale * event.gesture.scale;
  12627. this._zoom(scale, pointer)
  12628. };
  12629. /**
  12630. * Zoom the graph in or out
  12631. * @param {Number} scale a number around 1, and between 0.01 and 10
  12632. * @param {{x: Number, y: Number}} pointer
  12633. * @return {Number} appliedScale scale is limited within the boundaries
  12634. * @private
  12635. */
  12636. Graph.prototype._zoom = function(scale, pointer) {
  12637. var scaleOld = this._getScale();
  12638. if (scale < 0.01) {
  12639. scale = 0.01;
  12640. }
  12641. if (scale > 10) {
  12642. scale = 10;
  12643. }
  12644. var translation = this._getTranslation();
  12645. var scaleFrac = scale / scaleOld;
  12646. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  12647. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  12648. this._setScale(scale);
  12649. this._setTranslation(tx, ty);
  12650. this._redraw();
  12651. return scale;
  12652. };
  12653. /**
  12654. * Event handler for mouse wheel event, used to zoom the timeline
  12655. * See http://adomas.org/javascript-mouse-wheel/
  12656. * https://github.com/EightMedia/hammer.js/issues/256
  12657. * @param {MouseEvent} event
  12658. * @private
  12659. */
  12660. Graph.prototype._onMouseWheel = function(event) {
  12661. // retrieve delta
  12662. var delta = 0;
  12663. if (event.wheelDelta) { /* IE/Opera. */
  12664. delta = event.wheelDelta/120;
  12665. } else if (event.detail) { /* Mozilla case. */
  12666. // In Mozilla, sign of delta is different than in IE.
  12667. // Also, delta is multiple of 3.
  12668. delta = -event.detail/3;
  12669. }
  12670. // If delta is nonzero, handle it.
  12671. // Basically, delta is now positive if wheel was scrolled up,
  12672. // and negative, if wheel was scrolled down.
  12673. if (delta) {
  12674. if (!('mouswheelScale' in this.pinch)) {
  12675. this.pinch.mouswheelScale = 1;
  12676. }
  12677. // calculate the new scale
  12678. var scale = this.pinch.mouswheelScale;
  12679. var zoom = delta / 10;
  12680. if (delta < 0) {
  12681. zoom = zoom / (1 - zoom);
  12682. }
  12683. scale *= (1 + zoom);
  12684. // calculate the pointer location
  12685. var gesture = Hammer.event.collectEventData(this, 'scroll', event);
  12686. var pointer = this._getPointer(gesture.center);
  12687. // apply the new scale
  12688. scale = this._zoom(scale, pointer);
  12689. // store the new, applied scale
  12690. this.pinch.mouswheelScale = scale;
  12691. }
  12692. // Prevent default actions caused by mouse wheel.
  12693. event.preventDefault();
  12694. };
  12695. /**
  12696. * Mouse move handler for checking whether the title moves over a node with a title.
  12697. * @param {Event} event
  12698. * @private
  12699. */
  12700. Graph.prototype._onMouseMoveTitle = function (event) {
  12701. var gesture = Hammer.event.collectEventData(this, 'mousemove', event);
  12702. var pointer = this._getPointer(gesture.center);
  12703. // check if the previously selected node is still selected
  12704. if (this.popupNode) {
  12705. this._checkHidePopup(pointer);
  12706. }
  12707. // start a timeout that will check if the mouse is positioned above
  12708. // an element
  12709. var me = this;
  12710. var checkShow = function() {
  12711. me._checkShowPopup(pointer);
  12712. };
  12713. if (this.popupTimer) {
  12714. clearInterval(this.popupTimer); // stop any running timer
  12715. }
  12716. if (!this.leftButtonDown) {
  12717. this.popupTimer = setTimeout(checkShow, 300);
  12718. }
  12719. };
  12720. /**
  12721. * Check if there is an element on the given position in the graph
  12722. * (a node or edge). If so, and if this element has a title,
  12723. * show a popup window with its title.
  12724. *
  12725. * @param {{x:Number, y:Number}} pointer
  12726. * @private
  12727. */
  12728. Graph.prototype._checkShowPopup = function (pointer) {
  12729. var obj = {
  12730. left: this._canvasToX(pointer.x),
  12731. top: this._canvasToY(pointer.y),
  12732. right: this._canvasToX(pointer.x),
  12733. bottom: this._canvasToY(pointer.y)
  12734. };
  12735. var id;
  12736. var lastPopupNode = this.popupNode;
  12737. if (this.popupNode == undefined) {
  12738. // search the nodes for overlap, select the top one in case of multiple nodes
  12739. var nodes = this.nodes;
  12740. for (id in nodes) {
  12741. if (nodes.hasOwnProperty(id)) {
  12742. var node = nodes[id];
  12743. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  12744. this.popupNode = node;
  12745. break;
  12746. }
  12747. }
  12748. }
  12749. }
  12750. if (this.popupNode == undefined) {
  12751. // search the edges for overlap
  12752. var edges = this.edges;
  12753. for (id in edges) {
  12754. if (edges.hasOwnProperty(id)) {
  12755. var edge = edges[id];
  12756. if (edge.connected && (edge.getTitle() != undefined) &&
  12757. edge.isOverlappingWith(obj)) {
  12758. this.popupNode = edge;
  12759. break;
  12760. }
  12761. }
  12762. }
  12763. }
  12764. if (this.popupNode) {
  12765. // show popup message window
  12766. if (this.popupNode != lastPopupNode) {
  12767. var me = this;
  12768. if (!me.popup) {
  12769. me.popup = new Popup(me.frame);
  12770. }
  12771. // adjust a small offset such that the mouse cursor is located in the
  12772. // bottom left location of the popup, and you can easily move over the
  12773. // popup area
  12774. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  12775. me.popup.setText(me.popupNode.getTitle());
  12776. me.popup.show();
  12777. }
  12778. }
  12779. else {
  12780. if (this.popup) {
  12781. this.popup.hide();
  12782. }
  12783. }
  12784. };
  12785. /**
  12786. * Check if the popup must be hided, which is the case when the mouse is no
  12787. * longer hovering on the object
  12788. * @param {{x:Number, y:Number}} pointer
  12789. * @private
  12790. */
  12791. Graph.prototype._checkHidePopup = function (pointer) {
  12792. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  12793. this.popupNode = undefined;
  12794. if (this.popup) {
  12795. this.popup.hide();
  12796. }
  12797. }
  12798. };
  12799. /**
  12800. * Unselect selected nodes. If no selection array is provided, all nodes
  12801. * are unselected
  12802. * @param {Object[]} selection Array with selection objects, each selection
  12803. * object has a parameter row. Optional
  12804. * @param {Boolean} triggerSelect If true (default), the select event
  12805. * is triggered when nodes are unselected
  12806. * @return {Boolean} changed True if the selection is changed
  12807. * @private
  12808. */
  12809. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  12810. var changed = false;
  12811. var i, iMax, id;
  12812. if (selection) {
  12813. // remove provided selections
  12814. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12815. id = selection[i];
  12816. this.nodes[id].unselect();
  12817. var j = 0;
  12818. while (j < this.selection.length) {
  12819. if (this.selection[j] == id) {
  12820. this.selection.splice(j, 1);
  12821. changed = true;
  12822. }
  12823. else {
  12824. j++;
  12825. }
  12826. }
  12827. }
  12828. }
  12829. else if (this.selection && this.selection.length) {
  12830. // remove all selections
  12831. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  12832. id = this.selection[i];
  12833. this.nodes[id].unselect();
  12834. changed = true;
  12835. }
  12836. this.selection = [];
  12837. }
  12838. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  12839. // fire the select event
  12840. this._trigger('select');
  12841. }
  12842. return changed;
  12843. };
  12844. /**
  12845. * select all nodes on given location x, y
  12846. * @param {Array} selection an array with node ids
  12847. * @param {boolean} append If true, the new selection will be appended to the
  12848. * current selection (except for duplicate entries)
  12849. * @return {Boolean} changed True if the selection is changed
  12850. * @private
  12851. */
  12852. Graph.prototype._selectNodes = function(selection, append) {
  12853. var changed = false;
  12854. var i, iMax;
  12855. // TODO: the selectNodes method is a little messy, rework this
  12856. // check if the current selection equals the desired selection
  12857. var selectionAlreadyThere = true;
  12858. if (selection.length != this.selection.length) {
  12859. selectionAlreadyThere = false;
  12860. }
  12861. else {
  12862. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  12863. if (selection[i] != this.selection[i]) {
  12864. selectionAlreadyThere = false;
  12865. break;
  12866. }
  12867. }
  12868. }
  12869. if (selectionAlreadyThere) {
  12870. return changed;
  12871. }
  12872. if (append == undefined || append == false) {
  12873. // first deselect any selected node
  12874. var triggerSelect = false;
  12875. changed = this._unselectNodes(undefined, triggerSelect);
  12876. }
  12877. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12878. // add each of the new selections, but only when they are not duplicate
  12879. var id = selection[i];
  12880. var isDuplicate = (this.selection.indexOf(id) != -1);
  12881. if (!isDuplicate) {
  12882. this.nodes[id].select();
  12883. this.selection.push(id);
  12884. changed = true;
  12885. }
  12886. }
  12887. if (changed) {
  12888. // fire the select event
  12889. this._trigger('select');
  12890. }
  12891. return changed;
  12892. };
  12893. /**
  12894. * retrieve all nodes overlapping with given object
  12895. * @param {Object} obj An object with parameters left, top, right, bottom
  12896. * @return {Number[]} An array with id's of the overlapping nodes
  12897. * @private
  12898. */
  12899. Graph.prototype._getNodesOverlappingWith = function (obj) {
  12900. var nodes = this.nodes,
  12901. overlappingNodes = [];
  12902. for (var id in nodes) {
  12903. if (nodes.hasOwnProperty(id)) {
  12904. if (nodes[id].isOverlappingWith(obj)) {
  12905. overlappingNodes.push(id);
  12906. }
  12907. }
  12908. }
  12909. return overlappingNodes;
  12910. };
  12911. /**
  12912. * retrieve the currently selected nodes
  12913. * @return {Number[] | String[]} selection An array with the ids of the
  12914. * selected nodes.
  12915. */
  12916. Graph.prototype.getSelection = function() {
  12917. return this.selection.concat([]);
  12918. };
  12919. /**
  12920. * select zero or more nodes
  12921. * @param {Number[] | String[]} selection An array with the ids of the
  12922. * selected nodes.
  12923. */
  12924. Graph.prototype.setSelection = function(selection) {
  12925. var i, iMax, id;
  12926. if (!selection || (selection.length == undefined))
  12927. throw 'Selection must be an array with ids';
  12928. // first unselect any selected node
  12929. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  12930. id = this.selection[i];
  12931. this.nodes[id].unselect();
  12932. }
  12933. this.selection = [];
  12934. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12935. id = selection[i];
  12936. var node = this.nodes[id];
  12937. if (!node) {
  12938. throw new RangeError('Node with id "' + id + '" not found');
  12939. }
  12940. node.select();
  12941. this.selection.push(id);
  12942. }
  12943. this.redraw();
  12944. };
  12945. /**
  12946. * Validate the selection: remove ids of nodes which no longer exist
  12947. * @private
  12948. */
  12949. Graph.prototype._updateSelection = function () {
  12950. var i = 0;
  12951. while (i < this.selection.length) {
  12952. var id = this.selection[i];
  12953. if (!this.nodes[id]) {
  12954. this.selection.splice(i, 1);
  12955. }
  12956. else {
  12957. i++;
  12958. }
  12959. }
  12960. };
  12961. /**
  12962. * Temporary method to test calculating a hub value for the nodes
  12963. * @param {number} level Maximum number edges between two nodes in order
  12964. * to call them connected. Optional, 1 by default
  12965. * @return {Number[]} connectioncount array with the connection count
  12966. * for each node
  12967. * @private
  12968. */
  12969. Graph.prototype._getConnectionCount = function(level) {
  12970. if (level == undefined) {
  12971. level = 1;
  12972. }
  12973. // get the nodes connected to given nodes
  12974. function getConnectedNodes(nodes) {
  12975. var connectedNodes = [];
  12976. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  12977. var node = nodes[j];
  12978. // find all nodes connected to this node
  12979. var edges = node.edges;
  12980. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  12981. var edge = edges[i];
  12982. var other = null;
  12983. // check if connected
  12984. if (edge.from == node)
  12985. other = edge.to;
  12986. else if (edge.to == node)
  12987. other = edge.from;
  12988. // check if the other node is not already in the list with nodes
  12989. var k, kMax;
  12990. if (other) {
  12991. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  12992. if (nodes[k] == other) {
  12993. other = null;
  12994. break;
  12995. }
  12996. }
  12997. }
  12998. if (other) {
  12999. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  13000. if (connectedNodes[k] == other) {
  13001. other = null;
  13002. break;
  13003. }
  13004. }
  13005. }
  13006. if (other)
  13007. connectedNodes.push(other);
  13008. }
  13009. }
  13010. return connectedNodes;
  13011. }
  13012. var connections = [];
  13013. var nodes = this.nodes;
  13014. for (var id in nodes) {
  13015. if (nodes.hasOwnProperty(id)) {
  13016. var c = [nodes[id]];
  13017. for (var l = 0; l < level; l++) {
  13018. c = c.concat(getConnectedNodes(c));
  13019. }
  13020. connections.push(c);
  13021. }
  13022. }
  13023. var hubs = [];
  13024. for (var i = 0, len = connections.length; i < len; i++) {
  13025. hubs.push(connections[i].length);
  13026. }
  13027. return hubs;
  13028. };
  13029. /**
  13030. * Set a new size for the graph
  13031. * @param {string} width Width in pixels or percentage (for example '800px'
  13032. * or '50%')
  13033. * @param {string} height Height in pixels or percentage (for example '400px'
  13034. * or '30%')
  13035. */
  13036. Graph.prototype.setSize = function(width, height) {
  13037. this.frame.style.width = width;
  13038. this.frame.style.height = height;
  13039. this.frame.canvas.style.width = '100%';
  13040. this.frame.canvas.style.height = '100%';
  13041. this.frame.canvas.width = this.frame.canvas.clientWidth;
  13042. this.frame.canvas.height = this.frame.canvas.clientHeight;
  13043. };
  13044. /**
  13045. * Set a data set with nodes for the graph
  13046. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  13047. * @private
  13048. */
  13049. Graph.prototype._setNodes = function(nodes) {
  13050. var oldNodesData = this.nodesData;
  13051. if (nodes instanceof DataSet || nodes instanceof DataView) {
  13052. this.nodesData = nodes;
  13053. }
  13054. else if (nodes instanceof Array) {
  13055. this.nodesData = new DataSet();
  13056. this.nodesData.add(nodes);
  13057. }
  13058. else if (!nodes) {
  13059. this.nodesData = new DataSet();
  13060. }
  13061. else {
  13062. throw new TypeError('Array or DataSet expected');
  13063. }
  13064. if (oldNodesData) {
  13065. // unsubscribe from old dataset
  13066. util.forEach(this.nodesListeners, function (callback, event) {
  13067. oldNodesData.unsubscribe(event, callback);
  13068. });
  13069. }
  13070. // remove drawn nodes
  13071. this.nodes = {};
  13072. if (this.nodesData) {
  13073. // subscribe to new dataset
  13074. var me = this;
  13075. util.forEach(this.nodesListeners, function (callback, event) {
  13076. me.nodesData.subscribe(event, callback);
  13077. });
  13078. // draw all new nodes
  13079. var ids = this.nodesData.getIds();
  13080. this._addNodes(ids);
  13081. }
  13082. this._updateSelection();
  13083. };
  13084. /**
  13085. * Add nodes
  13086. * @param {Number[] | String[]} ids
  13087. * @private
  13088. */
  13089. Graph.prototype._addNodes = function(ids) {
  13090. var id;
  13091. for (var i = 0, len = ids.length; i < len; i++) {
  13092. id = ids[i];
  13093. var data = this.nodesData.get(id);
  13094. var node = new Node(data, this.images, this.groups, this.constants);
  13095. this.nodes[id] = node; // note: this may replace an existing node
  13096. if (!node.isFixed()) {
  13097. // TODO: position new nodes in a smarter way!
  13098. var radius = this.constants.edges.length * 2;
  13099. var count = ids.length;
  13100. var angle = 2 * Math.PI * (i / count);
  13101. node.x = radius * Math.cos(angle);
  13102. node.y = radius * Math.sin(angle);
  13103. // note: no not use node.isMoving() here, as that gives the current
  13104. // velocity of the node, which is zero after creation of the node.
  13105. this.moving = true;
  13106. }
  13107. }
  13108. this._reconnectEdges();
  13109. this._updateValueRange(this.nodes);
  13110. };
  13111. /**
  13112. * Update existing nodes, or create them when not yet existing
  13113. * @param {Number[] | String[]} ids
  13114. * @private
  13115. */
  13116. Graph.prototype._updateNodes = function(ids) {
  13117. var nodes = this.nodes,
  13118. nodesData = this.nodesData;
  13119. for (var i = 0, len = ids.length; i < len; i++) {
  13120. var id = ids[i];
  13121. var node = nodes[id];
  13122. var data = nodesData.get(id);
  13123. if (node) {
  13124. // update node
  13125. node.setProperties(data, this.constants);
  13126. }
  13127. else {
  13128. // create node
  13129. node = new Node(properties, this.images, this.groups, this.constants);
  13130. nodes[id] = node;
  13131. if (!node.isFixed()) {
  13132. this.moving = true;
  13133. }
  13134. }
  13135. }
  13136. this._reconnectEdges();
  13137. this._updateValueRange(nodes);
  13138. };
  13139. /**
  13140. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  13141. * @param {Number[] | String[]} ids
  13142. * @private
  13143. */
  13144. Graph.prototype._removeNodes = function(ids) {
  13145. var nodes = this.nodes;
  13146. for (var i = 0, len = ids.length; i < len; i++) {
  13147. var id = ids[i];
  13148. delete nodes[id];
  13149. }
  13150. this._reconnectEdges();
  13151. this._updateSelection();
  13152. this._updateValueRange(nodes);
  13153. };
  13154. /**
  13155. * Load edges by reading the data table
  13156. * @param {Array | DataSet | DataView} edges The data containing the edges.
  13157. * @private
  13158. * @private
  13159. */
  13160. Graph.prototype._setEdges = function(edges) {
  13161. var oldEdgesData = this.edgesData;
  13162. if (edges instanceof DataSet || edges instanceof DataView) {
  13163. this.edgesData = edges;
  13164. }
  13165. else if (edges instanceof Array) {
  13166. this.edgesData = new DataSet();
  13167. this.edgesData.add(edges);
  13168. }
  13169. else if (!edges) {
  13170. this.edgesData = new DataSet();
  13171. }
  13172. else {
  13173. throw new TypeError('Array or DataSet expected');
  13174. }
  13175. if (oldEdgesData) {
  13176. // unsubscribe from old dataset
  13177. util.forEach(this.edgesListeners, function (callback, event) {
  13178. oldEdgesData.unsubscribe(event, callback);
  13179. });
  13180. }
  13181. // remove drawn edges
  13182. this.edges = {};
  13183. if (this.edgesData) {
  13184. // subscribe to new dataset
  13185. var me = this;
  13186. util.forEach(this.edgesListeners, function (callback, event) {
  13187. me.edgesData.subscribe(event, callback);
  13188. });
  13189. // draw all new nodes
  13190. var ids = this.edgesData.getIds();
  13191. this._addEdges(ids);
  13192. }
  13193. this._reconnectEdges();
  13194. };
  13195. /**
  13196. * Add edges
  13197. * @param {Number[] | String[]} ids
  13198. * @private
  13199. */
  13200. Graph.prototype._addEdges = function (ids) {
  13201. var edges = this.edges,
  13202. edgesData = this.edgesData;
  13203. for (var i = 0, len = ids.length; i < len; i++) {
  13204. var id = ids[i];
  13205. var oldEdge = edges[id];
  13206. if (oldEdge) {
  13207. oldEdge.disconnect();
  13208. }
  13209. var data = edgesData.get(id);
  13210. edges[id] = new Edge(data, this, this.constants);
  13211. }
  13212. this.moving = true;
  13213. this._updateValueRange(edges);
  13214. };
  13215. /**
  13216. * Update existing edges, or create them when not yet existing
  13217. * @param {Number[] | String[]} ids
  13218. * @private
  13219. */
  13220. Graph.prototype._updateEdges = function (ids) {
  13221. var edges = this.edges,
  13222. edgesData = this.edgesData;
  13223. for (var i = 0, len = ids.length; i < len; i++) {
  13224. var id = ids[i];
  13225. var data = edgesData.get(id);
  13226. var edge = edges[id];
  13227. if (edge) {
  13228. // update edge
  13229. edge.disconnect();
  13230. edge.setProperties(data, this.constants);
  13231. edge.connect();
  13232. }
  13233. else {
  13234. // create edge
  13235. edge = new Edge(data, this, this.constants);
  13236. this.edges[id] = edge;
  13237. }
  13238. }
  13239. this.moving = true;
  13240. this._updateValueRange(edges);
  13241. };
  13242. /**
  13243. * Remove existing edges. Non existing ids will be ignored
  13244. * @param {Number[] | String[]} ids
  13245. * @private
  13246. */
  13247. Graph.prototype._removeEdges = function (ids) {
  13248. var edges = this.edges;
  13249. for (var i = 0, len = ids.length; i < len; i++) {
  13250. var id = ids[i];
  13251. var edge = edges[id];
  13252. if (edge) {
  13253. edge.disconnect();
  13254. delete edges[id];
  13255. }
  13256. }
  13257. this.moving = true;
  13258. this._updateValueRange(edges);
  13259. };
  13260. /**
  13261. * Reconnect all edges
  13262. * @private
  13263. */
  13264. Graph.prototype._reconnectEdges = function() {
  13265. var id,
  13266. nodes = this.nodes,
  13267. edges = this.edges;
  13268. for (id in nodes) {
  13269. if (nodes.hasOwnProperty(id)) {
  13270. nodes[id].edges = [];
  13271. }
  13272. }
  13273. for (id in edges) {
  13274. if (edges.hasOwnProperty(id)) {
  13275. var edge = edges[id];
  13276. edge.from = null;
  13277. edge.to = null;
  13278. edge.connect();
  13279. }
  13280. }
  13281. };
  13282. /**
  13283. * Update the values of all object in the given array according to the current
  13284. * value range of the objects in the array.
  13285. * @param {Object} obj An object containing a set of Edges or Nodes
  13286. * The objects must have a method getValue() and
  13287. * setValueRange(min, max).
  13288. * @private
  13289. */
  13290. Graph.prototype._updateValueRange = function(obj) {
  13291. var id;
  13292. // determine the range of the objects
  13293. var valueMin = undefined;
  13294. var valueMax = undefined;
  13295. for (id in obj) {
  13296. if (obj.hasOwnProperty(id)) {
  13297. var value = obj[id].getValue();
  13298. if (value !== undefined) {
  13299. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  13300. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  13301. }
  13302. }
  13303. }
  13304. // adjust the range of all objects
  13305. if (valueMin !== undefined && valueMax !== undefined) {
  13306. for (id in obj) {
  13307. if (obj.hasOwnProperty(id)) {
  13308. obj[id].setValueRange(valueMin, valueMax);
  13309. }
  13310. }
  13311. }
  13312. };
  13313. /**
  13314. * Redraw the graph with the current data
  13315. * chart will be resized too.
  13316. */
  13317. Graph.prototype.redraw = function() {
  13318. this.setSize(this.width, this.height);
  13319. this._redraw();
  13320. };
  13321. /**
  13322. * Redraw the graph with the current data
  13323. * @private
  13324. */
  13325. Graph.prototype._redraw = function() {
  13326. var ctx = this.frame.canvas.getContext('2d');
  13327. // clear the canvas
  13328. var w = this.frame.canvas.width;
  13329. var h = this.frame.canvas.height;
  13330. ctx.clearRect(0, 0, w, h);
  13331. // set scaling and translation
  13332. ctx.save();
  13333. ctx.translate(this.translation.x, this.translation.y);
  13334. ctx.scale(this.scale, this.scale);
  13335. this._drawEdges(ctx);
  13336. this._drawNodes(ctx);
  13337. // restore original scaling and translation
  13338. ctx.restore();
  13339. };
  13340. /**
  13341. * Set the translation of the graph
  13342. * @param {Number} offsetX Horizontal offset
  13343. * @param {Number} offsetY Vertical offset
  13344. * @private
  13345. */
  13346. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  13347. if (this.translation === undefined) {
  13348. this.translation = {
  13349. x: 0,
  13350. y: 0
  13351. };
  13352. }
  13353. if (offsetX !== undefined) {
  13354. this.translation.x = offsetX;
  13355. }
  13356. if (offsetY !== undefined) {
  13357. this.translation.y = offsetY;
  13358. }
  13359. };
  13360. /**
  13361. * Get the translation of the graph
  13362. * @return {Object} translation An object with parameters x and y, both a number
  13363. * @private
  13364. */
  13365. Graph.prototype._getTranslation = function() {
  13366. return {
  13367. x: this.translation.x,
  13368. y: this.translation.y
  13369. };
  13370. };
  13371. /**
  13372. * Scale the graph
  13373. * @param {Number} scale Scaling factor 1.0 is unscaled
  13374. * @private
  13375. */
  13376. Graph.prototype._setScale = function(scale) {
  13377. this.scale = scale;
  13378. };
  13379. /**
  13380. * Get the current scale of the graph
  13381. * @return {Number} scale Scaling factor 1.0 is unscaled
  13382. * @private
  13383. */
  13384. Graph.prototype._getScale = function() {
  13385. return this.scale;
  13386. };
  13387. /**
  13388. * Convert a horizontal point on the HTML canvas to the x-value of the model
  13389. * @param {number} x
  13390. * @returns {number}
  13391. * @private
  13392. */
  13393. Graph.prototype._canvasToX = function(x) {
  13394. return (x - this.translation.x) / this.scale;
  13395. };
  13396. /**
  13397. * Convert an x-value in the model to a horizontal point on the HTML canvas
  13398. * @param {number} x
  13399. * @returns {number}
  13400. * @private
  13401. */
  13402. Graph.prototype._xToCanvas = function(x) {
  13403. return x * this.scale + this.translation.x;
  13404. };
  13405. /**
  13406. * Convert a vertical point on the HTML canvas to the y-value of the model
  13407. * @param {number} y
  13408. * @returns {number}
  13409. * @private
  13410. */
  13411. Graph.prototype._canvasToY = function(y) {
  13412. return (y - this.translation.y) / this.scale;
  13413. };
  13414. /**
  13415. * Convert an y-value in the model to a vertical point on the HTML canvas
  13416. * @param {number} y
  13417. * @returns {number}
  13418. * @private
  13419. */
  13420. Graph.prototype._yToCanvas = function(y) {
  13421. return y * this.scale + this.translation.y ;
  13422. };
  13423. /**
  13424. * Redraw all nodes
  13425. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  13426. * @param {CanvasRenderingContext2D} ctx
  13427. * @private
  13428. */
  13429. Graph.prototype._drawNodes = function(ctx) {
  13430. // first draw the unselected nodes
  13431. var nodes = this.nodes;
  13432. var selected = [];
  13433. for (var id in nodes) {
  13434. if (nodes.hasOwnProperty(id)) {
  13435. if (nodes[id].isSelected()) {
  13436. selected.push(id);
  13437. }
  13438. else {
  13439. nodes[id].draw(ctx);
  13440. }
  13441. }
  13442. }
  13443. // draw the selected nodes on top
  13444. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  13445. nodes[selected[s]].draw(ctx);
  13446. }
  13447. };
  13448. /**
  13449. * Redraw all edges
  13450. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  13451. * @param {CanvasRenderingContext2D} ctx
  13452. * @private
  13453. */
  13454. Graph.prototype._drawEdges = function(ctx) {
  13455. var edges = this.edges;
  13456. for (var id in edges) {
  13457. if (edges.hasOwnProperty(id)) {
  13458. var edge = edges[id];
  13459. if (edge.connected) {
  13460. edges[id].draw(ctx);
  13461. }
  13462. }
  13463. }
  13464. };
  13465. /**
  13466. * Find a stable position for all nodes
  13467. * @private
  13468. */
  13469. Graph.prototype._doStabilize = function() {
  13470. var start = new Date();
  13471. // find stable position
  13472. var count = 0;
  13473. var vmin = this.constants.minVelocity;
  13474. var stable = false;
  13475. while (!stable && count < this.constants.maxIterations) {
  13476. this._calculateForces();
  13477. this._discreteStepNodes();
  13478. stable = !this._isMoving(vmin);
  13479. count++;
  13480. }
  13481. var end = new Date();
  13482. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  13483. };
  13484. /**
  13485. * Calculate the external forces acting on the nodes
  13486. * Forces are caused by: edges, repulsing forces between nodes, gravity
  13487. * @private
  13488. */
  13489. Graph.prototype._calculateForces = function() {
  13490. // create a local edge to the nodes and edges, that is faster
  13491. var id, dx, dy, angle, distance, fx, fy,
  13492. repulsingForce, springForce, length, edgeLength,
  13493. nodes = this.nodes,
  13494. edges = this.edges;
  13495. // gravity, add a small constant force to pull the nodes towards the center of
  13496. // the graph
  13497. // Also, the forces are reset to zero in this loop by using _setForce instead
  13498. // of _addForce
  13499. var gravity = 0.01,
  13500. gx = this.frame.canvas.clientWidth / 2,
  13501. gy = this.frame.canvas.clientHeight / 2;
  13502. for (id in nodes) {
  13503. if (nodes.hasOwnProperty(id)) {
  13504. var node = nodes[id];
  13505. dx = gx - node.x;
  13506. dy = gy - node.y;
  13507. angle = Math.atan2(dy, dx);
  13508. fx = Math.cos(angle) * gravity;
  13509. fy = Math.sin(angle) * gravity;
  13510. node._setForce(fx, fy);
  13511. }
  13512. }
  13513. // repulsing forces between nodes
  13514. var minimumDistance = this.constants.nodes.distance,
  13515. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  13516. for (var id1 in nodes) {
  13517. if (nodes.hasOwnProperty(id1)) {
  13518. var node1 = nodes[id1];
  13519. for (var id2 in nodes) {
  13520. if (nodes.hasOwnProperty(id2)) {
  13521. var node2 = nodes[id2];
  13522. // calculate normally distributed force
  13523. dx = node2.x - node1.x;
  13524. dy = node2.y - node1.y;
  13525. distance = Math.sqrt(dx * dx + dy * dy);
  13526. angle = Math.atan2(dy, dx);
  13527. // TODO: correct factor for repulsing force
  13528. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13529. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13530. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  13531. fx = Math.cos(angle) * repulsingForce;
  13532. fy = Math.sin(angle) * repulsingForce;
  13533. node1._addForce(-fx, -fy);
  13534. node2._addForce(fx, fy);
  13535. }
  13536. }
  13537. }
  13538. }
  13539. /* TODO: re-implement repulsion of edges
  13540. for (var n = 0; n < nodes.length; n++) {
  13541. for (var l = 0; l < edges.length; l++) {
  13542. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  13543. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  13544. // calculate normally distributed force
  13545. dx = nodes[n].x - lx,
  13546. dy = nodes[n].y - ly,
  13547. distance = Math.sqrt(dx * dx + dy * dy),
  13548. angle = Math.atan2(dy, dx),
  13549. // TODO: correct factor for repulsing force
  13550. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13551. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  13552. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  13553. fx = Math.cos(angle) * repulsingforce,
  13554. fy = Math.sin(angle) * repulsingforce;
  13555. nodes[n]._addForce(fx, fy);
  13556. edges[l].from._addForce(-fx/2,-fy/2);
  13557. edges[l].to._addForce(-fx/2,-fy/2);
  13558. }
  13559. }
  13560. */
  13561. // forces caused by the edges, modelled as springs
  13562. for (id in edges) {
  13563. if (edges.hasOwnProperty(id)) {
  13564. var edge = edges[id];
  13565. if (edge.connected) {
  13566. dx = (edge.to.x - edge.from.x);
  13567. dy = (edge.to.y - edge.from.y);
  13568. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  13569. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  13570. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  13571. edgeLength = edge.length;
  13572. length = Math.sqrt(dx * dx + dy * dy);
  13573. angle = Math.atan2(dy, dx);
  13574. springForce = edge.stiffness * (edgeLength - length);
  13575. fx = Math.cos(angle) * springForce;
  13576. fy = Math.sin(angle) * springForce;
  13577. edge.from._addForce(-fx, -fy);
  13578. edge.to._addForce(fx, fy);
  13579. }
  13580. }
  13581. }
  13582. /* TODO: re-implement repulsion of edges
  13583. // repulsing forces between edges
  13584. var minimumDistance = this.constants.edges.distance,
  13585. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  13586. for (var l = 0; l < edges.length; l++) {
  13587. //Keep distance from other edge centers
  13588. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  13589. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  13590. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  13591. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  13592. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  13593. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  13594. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  13595. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  13596. // calculate normally distributed force
  13597. dx = l2x - lx,
  13598. dy = l2y - ly,
  13599. distance = Math.sqrt(dx * dx + dy * dy),
  13600. angle = Math.atan2(dy, dx),
  13601. // TODO: correct factor for repulsing force
  13602. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13603. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  13604. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  13605. fx = Math.cos(angle) * repulsingforce,
  13606. fy = Math.sin(angle) * repulsingforce;
  13607. edges[l].from._addForce(-fx, -fy);
  13608. edges[l].to._addForce(-fx, -fy);
  13609. edges[l2].from._addForce(fx, fy);
  13610. edges[l2].to._addForce(fx, fy);
  13611. }
  13612. }
  13613. */
  13614. };
  13615. /**
  13616. * Check if any of the nodes is still moving
  13617. * @param {number} vmin the minimum velocity considered as 'moving'
  13618. * @return {boolean} true if moving, false if non of the nodes is moving
  13619. * @private
  13620. */
  13621. Graph.prototype._isMoving = function(vmin) {
  13622. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  13623. var nodes = this.nodes;
  13624. for (var id in nodes) {
  13625. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  13626. return true;
  13627. }
  13628. }
  13629. return false;
  13630. };
  13631. /**
  13632. * Perform one discrete step for all nodes
  13633. * @private
  13634. */
  13635. Graph.prototype._discreteStepNodes = function() {
  13636. var interval = this.refreshRate / 1000.0; // in seconds
  13637. var nodes = this.nodes;
  13638. for (var id in nodes) {
  13639. if (nodes.hasOwnProperty(id)) {
  13640. nodes[id].discreteStep(interval);
  13641. }
  13642. }
  13643. };
  13644. /**
  13645. * Start animating nodes and edges
  13646. */
  13647. Graph.prototype.start = function() {
  13648. if (this.moving) {
  13649. this._calculateForces();
  13650. this._discreteStepNodes();
  13651. var vmin = this.constants.minVelocity;
  13652. this.moving = this._isMoving(vmin);
  13653. }
  13654. if (this.moving) {
  13655. // start animation. only start timer if it is not already running
  13656. if (!this.timer) {
  13657. var graph = this;
  13658. this.timer = window.setTimeout(function () {
  13659. graph.timer = undefined;
  13660. graph.start();
  13661. graph._redraw();
  13662. }, this.refreshRate);
  13663. }
  13664. }
  13665. else {
  13666. this._redraw();
  13667. }
  13668. };
  13669. /**
  13670. * Stop animating nodes and edges.
  13671. */
  13672. Graph.prototype.stop = function () {
  13673. if (this.timer) {
  13674. window.clearInterval(this.timer);
  13675. this.timer = undefined;
  13676. }
  13677. };
  13678. /**
  13679. * vis.js module exports
  13680. */
  13681. var vis = {
  13682. util: util,
  13683. events: events,
  13684. Controller: Controller,
  13685. DataSet: DataSet,
  13686. DataView: DataView,
  13687. Range: Range,
  13688. Stack: Stack,
  13689. TimeStep: TimeStep,
  13690. EventBus: EventBus,
  13691. components: {
  13692. items: {
  13693. Item: Item,
  13694. ItemBox: ItemBox,
  13695. ItemPoint: ItemPoint,
  13696. ItemRange: ItemRange
  13697. },
  13698. Component: Component,
  13699. Panel: Panel,
  13700. RootPanel: RootPanel,
  13701. ItemSet: ItemSet,
  13702. TimeAxis: TimeAxis
  13703. },
  13704. graph: {
  13705. Node: Node,
  13706. Edge: Edge,
  13707. Popup: Popup,
  13708. Groups: Groups,
  13709. Images: Images
  13710. },
  13711. Timeline: Timeline,
  13712. Graph: Graph
  13713. };
  13714. /**
  13715. * CommonJS module exports
  13716. */
  13717. if (typeof exports !== 'undefined') {
  13718. exports = vis;
  13719. }
  13720. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  13721. module.exports = vis;
  13722. }
  13723. /**
  13724. * AMD module exports
  13725. */
  13726. if (typeof(define) === 'function') {
  13727. define(function () {
  13728. return vis;
  13729. });
  13730. }
  13731. /**
  13732. * Window exports
  13733. */
  13734. if (typeof window !== 'undefined') {
  13735. // attach the module to the window, load as a regular javascript file
  13736. window['vis'] = vis;
  13737. }
  13738. // inject css
  13739. util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.rangeoverflow {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n bo
  13740. },{"hammerjs":1,"moment":2}]},{},[3])
  13741. (3)
  13742. });
  13743. ;