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.

15764 lines
439 KiB

  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.3.0
  8. * @date 2014-01-14
  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){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.vis=e()}}(function(){var define,module,exports;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){
  26. /**
  27. * vis.js module imports
  28. */
  29. // Try to load dependencies from the global window object.
  30. // If not available there, load via require.
  31. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  32. var Hammer;
  33. if (typeof window !== 'undefined') {
  34. // load hammer.js only when running in a browser (where window is available)
  35. Hammer = window['Hammer'] || require('hammerjs');
  36. }
  37. else {
  38. Hammer = function () {
  39. throw Error('hammer.js is only available in a browser, not in node.js.');
  40. }
  41. }
  42. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  43. // it here in that case.
  44. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  45. if(!Array.prototype.indexOf) {
  46. Array.prototype.indexOf = function(obj){
  47. for(var i = 0; i < this.length; i++){
  48. if(this[i] == obj){
  49. return i;
  50. }
  51. }
  52. return -1;
  53. };
  54. try {
  55. console.log("Warning: Ancient browser detected. Please update your browser");
  56. }
  57. catch (err) {
  58. }
  59. }
  60. // Internet Explorer 8 and older does not support Array.forEach, so we define
  61. // it here in that case.
  62. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  63. if (!Array.prototype.forEach) {
  64. Array.prototype.forEach = function(fn, scope) {
  65. for(var i = 0, len = this.length; i < len; ++i) {
  66. fn.call(scope || this, this[i], i, this);
  67. }
  68. }
  69. }
  70. // Internet Explorer 8 and older does not support Array.map, so we define it
  71. // here in that case.
  72. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  73. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  74. // Reference: http://es5.github.com/#x15.4.4.19
  75. if (!Array.prototype.map) {
  76. Array.prototype.map = function(callback, thisArg) {
  77. var T, A, k;
  78. if (this == null) {
  79. throw new TypeError(" this is null or not defined");
  80. }
  81. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  82. var O = Object(this);
  83. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  84. // 3. Let len be ToUint32(lenValue).
  85. var len = O.length >>> 0;
  86. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  87. // See: http://es5.github.com/#x9.11
  88. if (typeof callback !== "function") {
  89. throw new TypeError(callback + " is not a function");
  90. }
  91. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  92. if (thisArg) {
  93. T = thisArg;
  94. }
  95. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  96. // the standard built-in constructor with that name and len is the value of len.
  97. A = new Array(len);
  98. // 7. Let k be 0
  99. k = 0;
  100. // 8. Repeat, while k < len
  101. while(k < len) {
  102. var kValue, mappedValue;
  103. // a. Let Pk be ToString(k).
  104. // This is implicit for LHS operands of the in operator
  105. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  106. // This step can be combined with c
  107. // c. If kPresent is true, then
  108. if (k in O) {
  109. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  110. kValue = O[ k ];
  111. // ii. Let mappedValue be the result of calling the Call internal method of callback
  112. // with T as the this value and argument list containing kValue, k, and O.
  113. mappedValue = callback.call(T, kValue, k, O);
  114. // iii. Call the DefineOwnProperty internal method of A with arguments
  115. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  116. // and false.
  117. // In browsers that support Object.defineProperty, use the following:
  118. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  119. // For best browser support, use the following:
  120. A[ k ] = mappedValue;
  121. }
  122. // d. Increase k by 1.
  123. k++;
  124. }
  125. // 9. return A
  126. return A;
  127. };
  128. }
  129. // Internet Explorer 8 and older does not support Array.filter, so we define it
  130. // here in that case.
  131. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  132. if (!Array.prototype.filter) {
  133. Array.prototype.filter = function(fun /*, thisp */) {
  134. "use strict";
  135. if (this == null) {
  136. throw new TypeError();
  137. }
  138. var t = Object(this);
  139. var len = t.length >>> 0;
  140. if (typeof fun != "function") {
  141. throw new TypeError();
  142. }
  143. var res = [];
  144. var thisp = arguments[1];
  145. for (var i = 0; i < len; i++) {
  146. if (i in t) {
  147. var val = t[i]; // in case fun mutates this
  148. if (fun.call(thisp, val, i, t))
  149. res.push(val);
  150. }
  151. }
  152. return res;
  153. };
  154. }
  155. // Internet Explorer 8 and older does not support Object.keys, so we define it
  156. // here in that case.
  157. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  158. if (!Object.keys) {
  159. Object.keys = (function () {
  160. var hasOwnProperty = Object.prototype.hasOwnProperty,
  161. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  162. dontEnums = [
  163. 'toString',
  164. 'toLocaleString',
  165. 'valueOf',
  166. 'hasOwnProperty',
  167. 'isPrototypeOf',
  168. 'propertyIsEnumerable',
  169. 'constructor'
  170. ],
  171. dontEnumsLength = dontEnums.length;
  172. return function (obj) {
  173. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  174. throw new TypeError('Object.keys called on non-object');
  175. }
  176. var result = [];
  177. for (var prop in obj) {
  178. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  179. }
  180. if (hasDontEnumBug) {
  181. for (var i=0; i < dontEnumsLength; i++) {
  182. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  183. }
  184. }
  185. return result;
  186. }
  187. })()
  188. }
  189. // Internet Explorer 8 and older does not support Array.isArray,
  190. // so we define it here in that case.
  191. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  192. if(!Array.isArray) {
  193. Array.isArray = function (vArg) {
  194. return Object.prototype.toString.call(vArg) === "[object Array]";
  195. };
  196. }
  197. // Internet Explorer 8 and older does not support Function.bind,
  198. // so we define it here in that case.
  199. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  200. if (!Function.prototype.bind) {
  201. Function.prototype.bind = function (oThis) {
  202. if (typeof this !== "function") {
  203. // closest thing possible to the ECMAScript 5 internal IsCallable function
  204. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  205. }
  206. var aArgs = Array.prototype.slice.call(arguments, 1),
  207. fToBind = this,
  208. fNOP = function () {},
  209. fBound = function () {
  210. return fToBind.apply(this instanceof fNOP && oThis
  211. ? this
  212. : oThis,
  213. aArgs.concat(Array.prototype.slice.call(arguments)));
  214. };
  215. fNOP.prototype = this.prototype;
  216. fBound.prototype = new fNOP();
  217. return fBound;
  218. };
  219. }
  220. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  221. if (!Object.create) {
  222. Object.create = function (o) {
  223. if (arguments.length > 1) {
  224. throw new Error('Object.create implementation only accepts the first parameter.');
  225. }
  226. function F() {}
  227. F.prototype = o;
  228. return new F();
  229. };
  230. }
  231. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  232. if (!Function.prototype.bind) {
  233. Function.prototype.bind = function (oThis) {
  234. if (typeof this !== "function") {
  235. // closest thing possible to the ECMAScript 5 internal IsCallable function
  236. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  237. }
  238. var aArgs = Array.prototype.slice.call(arguments, 1),
  239. fToBind = this,
  240. fNOP = function () {},
  241. fBound = function () {
  242. return fToBind.apply(this instanceof fNOP && oThis
  243. ? this
  244. : oThis,
  245. aArgs.concat(Array.prototype.slice.call(arguments)));
  246. };
  247. fNOP.prototype = this.prototype;
  248. fBound.prototype = new fNOP();
  249. return fBound;
  250. };
  251. }
  252. /**
  253. * utility functions
  254. */
  255. var util = {};
  256. /**
  257. * Test whether given object is a number
  258. * @param {*} object
  259. * @return {Boolean} isNumber
  260. */
  261. util.isNumber = function isNumber(object) {
  262. return (object instanceof Number || typeof object == 'number');
  263. };
  264. /**
  265. * Test whether given object is a string
  266. * @param {*} object
  267. * @return {Boolean} isString
  268. */
  269. util.isString = function isString(object) {
  270. return (object instanceof String || typeof object == 'string');
  271. };
  272. /**
  273. * Test whether given object is a Date, or a String containing a Date
  274. * @param {Date | String} object
  275. * @return {Boolean} isDate
  276. */
  277. util.isDate = function isDate(object) {
  278. if (object instanceof Date) {
  279. return true;
  280. }
  281. else if (util.isString(object)) {
  282. // test whether this string contains a date
  283. var match = ASPDateRegex.exec(object);
  284. if (match) {
  285. return true;
  286. }
  287. else if (!isNaN(Date.parse(object))) {
  288. return true;
  289. }
  290. }
  291. return false;
  292. };
  293. /**
  294. * Test whether given object is an instance of google.visualization.DataTable
  295. * @param {*} object
  296. * @return {Boolean} isDataTable
  297. */
  298. util.isDataTable = function isDataTable(object) {
  299. return (typeof (google) !== 'undefined') &&
  300. (google.visualization) &&
  301. (google.visualization.DataTable) &&
  302. (object instanceof google.visualization.DataTable);
  303. };
  304. /**
  305. * Create a semi UUID
  306. * source: http://stackoverflow.com/a/105074/1262753
  307. * @return {String} uuid
  308. */
  309. util.randomUUID = function randomUUID () {
  310. var S4 = function () {
  311. return Math.floor(
  312. Math.random() * 0x10000 /* 65536 */
  313. ).toString(16);
  314. };
  315. return (
  316. S4() + S4() + '-' +
  317. S4() + '-' +
  318. S4() + '-' +
  319. S4() + '-' +
  320. S4() + S4() + S4()
  321. );
  322. };
  323. /**
  324. * Extend object a with the properties of object b or a series of objects
  325. * Only properties with defined values are copied
  326. * @param {Object} a
  327. * @param {... Object} b
  328. * @return {Object} a
  329. */
  330. util.extend = function (a, b) {
  331. for (var i = 1, len = arguments.length; i < len; i++) {
  332. var other = arguments[i];
  333. for (var prop in other) {
  334. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  335. a[prop] = other[prop];
  336. }
  337. }
  338. }
  339. return a;
  340. };
  341. /**
  342. * Convert an object to another type
  343. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  344. * @param {String | undefined} type Name of the type. Available types:
  345. * 'Boolean', 'Number', 'String',
  346. * 'Date', 'Moment', ISODate', 'ASPDate'.
  347. * @return {*} object
  348. * @throws Error
  349. */
  350. util.convert = function convert(object, type) {
  351. var match;
  352. if (object === undefined) {
  353. return undefined;
  354. }
  355. if (object === null) {
  356. return null;
  357. }
  358. if (!type) {
  359. return object;
  360. }
  361. if (!(typeof type === 'string') && !(type instanceof String)) {
  362. throw new Error('Type must be a string');
  363. }
  364. //noinspection FallthroughInSwitchStatementJS
  365. switch (type) {
  366. case 'boolean':
  367. case 'Boolean':
  368. return Boolean(object);
  369. case 'number':
  370. case 'Number':
  371. return Number(object.valueOf());
  372. case 'string':
  373. case 'String':
  374. return String(object);
  375. case 'Date':
  376. if (util.isNumber(object)) {
  377. return new Date(object);
  378. }
  379. if (object instanceof Date) {
  380. return new Date(object.valueOf());
  381. }
  382. else if (moment.isMoment(object)) {
  383. return new Date(object.valueOf());
  384. }
  385. if (util.isString(object)) {
  386. match = ASPDateRegex.exec(object);
  387. if (match) {
  388. // object is an ASP date
  389. return new Date(Number(match[1])); // parse number
  390. }
  391. else {
  392. return moment(object).toDate(); // parse string
  393. }
  394. }
  395. else {
  396. throw new Error(
  397. 'Cannot convert object of type ' + util.getType(object) +
  398. ' to type Date');
  399. }
  400. case 'Moment':
  401. if (util.isNumber(object)) {
  402. return moment(object);
  403. }
  404. if (object instanceof Date) {
  405. return moment(object.valueOf());
  406. }
  407. else if (moment.isMoment(object)) {
  408. return moment(object);
  409. }
  410. if (util.isString(object)) {
  411. match = ASPDateRegex.exec(object);
  412. if (match) {
  413. // object is an ASP date
  414. return moment(Number(match[1])); // parse number
  415. }
  416. else {
  417. return moment(object); // parse string
  418. }
  419. }
  420. else {
  421. throw new Error(
  422. 'Cannot convert object of type ' + util.getType(object) +
  423. ' to type Date');
  424. }
  425. case 'ISODate':
  426. if (util.isNumber(object)) {
  427. return new Date(object);
  428. }
  429. else if (object instanceof Date) {
  430. return object.toISOString();
  431. }
  432. else if (moment.isMoment(object)) {
  433. return object.toDate().toISOString();
  434. }
  435. else if (util.isString(object)) {
  436. match = ASPDateRegex.exec(object);
  437. if (match) {
  438. // object is an ASP date
  439. return new Date(Number(match[1])).toISOString(); // parse number
  440. }
  441. else {
  442. return new Date(object).toISOString(); // parse string
  443. }
  444. }
  445. else {
  446. throw new Error(
  447. 'Cannot convert object of type ' + util.getType(object) +
  448. ' to type ISODate');
  449. }
  450. case 'ASPDate':
  451. if (util.isNumber(object)) {
  452. return '/Date(' + object + ')/';
  453. }
  454. else if (object instanceof Date) {
  455. return '/Date(' + object.valueOf() + ')/';
  456. }
  457. else if (util.isString(object)) {
  458. match = ASPDateRegex.exec(object);
  459. var value;
  460. if (match) {
  461. // object is an ASP date
  462. value = new Date(Number(match[1])).valueOf(); // parse number
  463. }
  464. else {
  465. value = new Date(object).valueOf(); // parse string
  466. }
  467. return '/Date(' + value + ')/';
  468. }
  469. else {
  470. throw new Error(
  471. 'Cannot convert object of type ' + util.getType(object) +
  472. ' to type ASPDate');
  473. }
  474. default:
  475. throw new Error('Cannot convert object of type ' + util.getType(object) +
  476. ' to type "' + type + '"');
  477. }
  478. };
  479. // parse ASP.Net Date pattern,
  480. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  481. // code from http://momentjs.com/
  482. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  483. /**
  484. * Get the type of an object, for example util.getType([]) returns 'Array'
  485. * @param {*} object
  486. * @return {String} type
  487. */
  488. util.getType = function getType(object) {
  489. var type = typeof object;
  490. if (type == 'object') {
  491. if (object == null) {
  492. return 'null';
  493. }
  494. if (object instanceof Boolean) {
  495. return 'Boolean';
  496. }
  497. if (object instanceof Number) {
  498. return 'Number';
  499. }
  500. if (object instanceof String) {
  501. return 'String';
  502. }
  503. if (object instanceof Array) {
  504. return 'Array';
  505. }
  506. if (object instanceof Date) {
  507. return 'Date';
  508. }
  509. return 'Object';
  510. }
  511. else if (type == 'number') {
  512. return 'Number';
  513. }
  514. else if (type == 'boolean') {
  515. return 'Boolean';
  516. }
  517. else if (type == 'string') {
  518. return 'String';
  519. }
  520. return type;
  521. };
  522. /**
  523. * Retrieve the absolute left value of a DOM element
  524. * @param {Element} elem A dom element, for example a div
  525. * @return {number} left The absolute left position of this element
  526. * in the browser page.
  527. */
  528. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  529. var doc = document.documentElement;
  530. var body = document.body;
  531. var left = elem.offsetLeft;
  532. var e = elem.offsetParent;
  533. while (e != null && e != body && e != doc) {
  534. left += e.offsetLeft;
  535. left -= e.scrollLeft;
  536. e = e.offsetParent;
  537. }
  538. return left;
  539. };
  540. /**
  541. * Retrieve the absolute top value of a DOM element
  542. * @param {Element} elem A dom element, for example a div
  543. * @return {number} top The absolute top position of this element
  544. * in the browser page.
  545. */
  546. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  547. var doc = document.documentElement;
  548. var body = document.body;
  549. var top = elem.offsetTop;
  550. var e = elem.offsetParent;
  551. while (e != null && e != body && e != doc) {
  552. top += e.offsetTop;
  553. top -= e.scrollTop;
  554. e = e.offsetParent;
  555. }
  556. return top;
  557. };
  558. /**
  559. * Get the absolute, vertical mouse position from an event.
  560. * @param {Event} event
  561. * @return {Number} pageY
  562. */
  563. util.getPageY = function getPageY (event) {
  564. if ('pageY' in event) {
  565. return event.pageY;
  566. }
  567. else {
  568. var clientY;
  569. if (('targetTouches' in event) && event.targetTouches.length) {
  570. clientY = event.targetTouches[0].clientY;
  571. }
  572. else {
  573. clientY = event.clientY;
  574. }
  575. var doc = document.documentElement;
  576. var body = document.body;
  577. return clientY +
  578. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  579. ( doc && doc.clientTop || body && body.clientTop || 0 );
  580. }
  581. };
  582. /**
  583. * Get the absolute, horizontal mouse position from an event.
  584. * @param {Event} event
  585. * @return {Number} pageX
  586. */
  587. util.getPageX = function getPageX (event) {
  588. if ('pageY' in event) {
  589. return event.pageX;
  590. }
  591. else {
  592. var clientX;
  593. if (('targetTouches' in event) && event.targetTouches.length) {
  594. clientX = event.targetTouches[0].clientX;
  595. }
  596. else {
  597. clientX = event.clientX;
  598. }
  599. var doc = document.documentElement;
  600. var body = document.body;
  601. return clientX +
  602. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  603. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  604. }
  605. };
  606. /**
  607. * add a className to the given elements style
  608. * @param {Element} elem
  609. * @param {String} className
  610. */
  611. util.addClassName = function addClassName(elem, className) {
  612. var classes = elem.className.split(' ');
  613. if (classes.indexOf(className) == -1) {
  614. classes.push(className); // add the class to the array
  615. elem.className = classes.join(' ');
  616. }
  617. };
  618. /**
  619. * add a className to the given elements style
  620. * @param {Element} elem
  621. * @param {String} className
  622. */
  623. util.removeClassName = function removeClassname(elem, className) {
  624. var classes = elem.className.split(' ');
  625. var index = classes.indexOf(className);
  626. if (index != -1) {
  627. classes.splice(index, 1); // remove the class from the array
  628. elem.className = classes.join(' ');
  629. }
  630. };
  631. /**
  632. * For each method for both arrays and objects.
  633. * In case of an array, the built-in Array.forEach() is applied.
  634. * In case of an Object, the method loops over all properties of the object.
  635. * @param {Object | Array} object An Object or Array
  636. * @param {function} callback Callback method, called for each item in
  637. * the object or array with three parameters:
  638. * callback(value, index, object)
  639. */
  640. util.forEach = function forEach (object, callback) {
  641. var i,
  642. len;
  643. if (object instanceof Array) {
  644. // array
  645. for (i = 0, len = object.length; i < len; i++) {
  646. callback(object[i], i, object);
  647. }
  648. }
  649. else {
  650. // object
  651. for (i in object) {
  652. if (object.hasOwnProperty(i)) {
  653. callback(object[i], i, object);
  654. }
  655. }
  656. }
  657. };
  658. /**
  659. * Update a property in an object
  660. * @param {Object} object
  661. * @param {String} key
  662. * @param {*} value
  663. * @return {Boolean} changed
  664. */
  665. util.updateProperty = function updateProp (object, key, value) {
  666. if (object[key] !== value) {
  667. object[key] = value;
  668. return true;
  669. }
  670. else {
  671. return false;
  672. }
  673. };
  674. /**
  675. * Add and event listener. Works for all browsers
  676. * @param {Element} element An html element
  677. * @param {string} action The action, for example "click",
  678. * without the prefix "on"
  679. * @param {function} listener The callback function to be executed
  680. * @param {boolean} [useCapture]
  681. */
  682. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  683. if (element.addEventListener) {
  684. if (useCapture === undefined)
  685. useCapture = false;
  686. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  687. action = "DOMMouseScroll"; // For Firefox
  688. }
  689. element.addEventListener(action, listener, useCapture);
  690. } else {
  691. element.attachEvent("on" + action, listener); // IE browsers
  692. }
  693. };
  694. /**
  695. * Remove an event listener from an element
  696. * @param {Element} element An html dom element
  697. * @param {string} action The name of the event, for example "mousedown"
  698. * @param {function} listener The listener function
  699. * @param {boolean} [useCapture]
  700. */
  701. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  702. if (element.removeEventListener) {
  703. // non-IE browsers
  704. if (useCapture === undefined)
  705. useCapture = false;
  706. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  707. action = "DOMMouseScroll"; // For Firefox
  708. }
  709. element.removeEventListener(action, listener, useCapture);
  710. } else {
  711. // IE browsers
  712. element.detachEvent("on" + action, listener);
  713. }
  714. };
  715. /**
  716. * Get HTML element which is the target of the event
  717. * @param {Event} event
  718. * @return {Element} target element
  719. */
  720. util.getTarget = function getTarget(event) {
  721. // code from http://www.quirksmode.org/js/events_properties.html
  722. if (!event) {
  723. event = window.event;
  724. }
  725. var target;
  726. if (event.target) {
  727. target = event.target;
  728. }
  729. else if (event.srcElement) {
  730. target = event.srcElement;
  731. }
  732. if (target.nodeType != undefined && target.nodeType == 3) {
  733. // defeat Safari bug
  734. target = target.parentNode;
  735. }
  736. return target;
  737. };
  738. /**
  739. * Stop event propagation
  740. */
  741. util.stopPropagation = function stopPropagation(event) {
  742. if (!event)
  743. event = window.event;
  744. if (event.stopPropagation) {
  745. event.stopPropagation(); // non-IE browsers
  746. }
  747. else {
  748. event.cancelBubble = true; // IE browsers
  749. }
  750. };
  751. /**
  752. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  753. * @param {Element} element
  754. * @param {Event} event
  755. */
  756. util.fakeGesture = function fakeGesture (element, event) {
  757. var eventType = null;
  758. // for hammer.js 1.0.5
  759. return Hammer.event.collectEventData(this, eventType, event);
  760. // for hammer.js 1.0.6
  761. //var touches = Hammer.event.getTouchList(event, eventType);
  762. //return Hammer.event.collectEventData(this, eventType, touches, event);
  763. };
  764. /**
  765. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  766. */
  767. util.preventDefault = function preventDefault (event) {
  768. if (!event)
  769. event = window.event;
  770. if (event.preventDefault) {
  771. event.preventDefault(); // non-IE browsers
  772. }
  773. else {
  774. event.returnValue = false; // IE browsers
  775. }
  776. };
  777. util.option = {};
  778. /**
  779. * Convert a value into a boolean
  780. * @param {Boolean | function | undefined} value
  781. * @param {Boolean} [defaultValue]
  782. * @returns {Boolean} bool
  783. */
  784. util.option.asBoolean = function (value, defaultValue) {
  785. if (typeof value == 'function') {
  786. value = value();
  787. }
  788. if (value != null) {
  789. return (value != false);
  790. }
  791. return defaultValue || null;
  792. };
  793. /**
  794. * Convert a value into a number
  795. * @param {Boolean | function | undefined} value
  796. * @param {Number} [defaultValue]
  797. * @returns {Number} number
  798. */
  799. util.option.asNumber = function (value, defaultValue) {
  800. if (typeof value == 'function') {
  801. value = value();
  802. }
  803. if (value != null) {
  804. return Number(value) || defaultValue || null;
  805. }
  806. return defaultValue || null;
  807. };
  808. /**
  809. * Convert a value into a string
  810. * @param {String | function | undefined} value
  811. * @param {String} [defaultValue]
  812. * @returns {String} str
  813. */
  814. util.option.asString = function (value, defaultValue) {
  815. if (typeof value == 'function') {
  816. value = value();
  817. }
  818. if (value != null) {
  819. return String(value);
  820. }
  821. return defaultValue || null;
  822. };
  823. /**
  824. * Convert a size or location into a string with pixels or a percentage
  825. * @param {String | Number | function | undefined} value
  826. * @param {String} [defaultValue]
  827. * @returns {String} size
  828. */
  829. util.option.asSize = function (value, defaultValue) {
  830. if (typeof value == 'function') {
  831. value = value();
  832. }
  833. if (util.isString(value)) {
  834. return value;
  835. }
  836. else if (util.isNumber(value)) {
  837. return value + 'px';
  838. }
  839. else {
  840. return defaultValue || null;
  841. }
  842. };
  843. /**
  844. * Convert a value into a DOM element
  845. * @param {HTMLElement | function | undefined} value
  846. * @param {HTMLElement} [defaultValue]
  847. * @returns {HTMLElement | null} dom
  848. */
  849. util.option.asElement = function (value, defaultValue) {
  850. if (typeof value == 'function') {
  851. value = value();
  852. }
  853. return value || defaultValue || null;
  854. };
  855. /**
  856. * Event listener (singleton)
  857. */
  858. // TODO: replace usage of the event listener for the EventBus
  859. var events = {
  860. 'listeners': [],
  861. /**
  862. * Find a single listener by its object
  863. * @param {Object} object
  864. * @return {Number} index -1 when not found
  865. */
  866. 'indexOf': function (object) {
  867. var listeners = this.listeners;
  868. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  869. var listener = listeners[i];
  870. if (listener && listener.object == object) {
  871. return i;
  872. }
  873. }
  874. return -1;
  875. },
  876. /**
  877. * Add an event listener
  878. * @param {Object} object
  879. * @param {String} event The name of an event, for example 'select'
  880. * @param {function} callback The callback method, called when the
  881. * event takes place
  882. */
  883. 'addListener': function (object, event, callback) {
  884. var index = this.indexOf(object);
  885. var listener = this.listeners[index];
  886. if (!listener) {
  887. listener = {
  888. 'object': object,
  889. 'events': {}
  890. };
  891. this.listeners.push(listener);
  892. }
  893. var callbacks = listener.events[event];
  894. if (!callbacks) {
  895. callbacks = [];
  896. listener.events[event] = callbacks;
  897. }
  898. // add the callback if it does not yet exist
  899. if (callbacks.indexOf(callback) == -1) {
  900. callbacks.push(callback);
  901. }
  902. },
  903. /**
  904. * Remove an event listener
  905. * @param {Object} object
  906. * @param {String} event The name of an event, for example 'select'
  907. * @param {function} callback The registered callback method
  908. */
  909. 'removeListener': function (object, event, callback) {
  910. var index = this.indexOf(object);
  911. var listener = this.listeners[index];
  912. if (listener) {
  913. var callbacks = listener.events[event];
  914. if (callbacks) {
  915. index = callbacks.indexOf(callback);
  916. if (index != -1) {
  917. callbacks.splice(index, 1);
  918. }
  919. // remove the array when empty
  920. if (callbacks.length == 0) {
  921. delete listener.events[event];
  922. }
  923. }
  924. // count the number of registered events. remove listener when empty
  925. var count = 0;
  926. var events = listener.events;
  927. for (var e in events) {
  928. if (events.hasOwnProperty(e)) {
  929. count++;
  930. }
  931. }
  932. if (count == 0) {
  933. delete this.listeners[index];
  934. }
  935. }
  936. },
  937. /**
  938. * Remove all registered event listeners
  939. */
  940. 'removeAllListeners': function () {
  941. this.listeners = [];
  942. },
  943. /**
  944. * Trigger an event. All registered event handlers will be called
  945. * @param {Object} object
  946. * @param {String} event
  947. * @param {Object} properties (optional)
  948. */
  949. 'trigger': function (object, event, properties) {
  950. var index = this.indexOf(object);
  951. var listener = this.listeners[index];
  952. if (listener) {
  953. var callbacks = listener.events[event];
  954. if (callbacks) {
  955. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  956. callbacks[i](properties);
  957. }
  958. }
  959. }
  960. }
  961. };
  962. /**
  963. * An event bus can be used to emit events, and to subscribe to events
  964. * @constructor EventBus
  965. */
  966. function EventBus() {
  967. this.subscriptions = [];
  968. }
  969. /**
  970. * Subscribe to an event
  971. * @param {String | RegExp} event The event can be a regular expression, or
  972. * a string with wildcards, like 'server.*'.
  973. * @param {function} callback. Callback are called with three parameters:
  974. * {String} event, {*} [data], {*} [source]
  975. * @param {*} [target]
  976. * @returns {String} id A subscription id
  977. */
  978. EventBus.prototype.on = function (event, callback, target) {
  979. var regexp = (event instanceof RegExp) ?
  980. event :
  981. new RegExp(event.replace('*', '\\w+'));
  982. var subscription = {
  983. id: util.randomUUID(),
  984. event: event,
  985. regexp: regexp,
  986. callback: (typeof callback === 'function') ? callback : null,
  987. target: target
  988. };
  989. this.subscriptions.push(subscription);
  990. return subscription.id;
  991. };
  992. /**
  993. * Unsubscribe from an event
  994. * @param {String | Object} filter Filter for subscriptions to be removed
  995. * Filter can be a string containing a
  996. * subscription id, or an object containing
  997. * one or more of the fields id, event,
  998. * callback, and target.
  999. */
  1000. EventBus.prototype.off = function (filter) {
  1001. var i = 0;
  1002. while (i < this.subscriptions.length) {
  1003. var subscription = this.subscriptions[i];
  1004. var match = true;
  1005. if (filter instanceof Object) {
  1006. // filter is an object. All fields must match
  1007. for (var prop in filter) {
  1008. if (filter.hasOwnProperty(prop)) {
  1009. if (filter[prop] !== subscription[prop]) {
  1010. match = false;
  1011. }
  1012. }
  1013. }
  1014. }
  1015. else {
  1016. // filter is a string, filter on id
  1017. match = (subscription.id == filter);
  1018. }
  1019. if (match) {
  1020. this.subscriptions.splice(i, 1);
  1021. }
  1022. else {
  1023. i++;
  1024. }
  1025. }
  1026. };
  1027. /**
  1028. * Emit an event
  1029. * @param {String} event
  1030. * @param {*} [data]
  1031. * @param {*} [source]
  1032. */
  1033. EventBus.prototype.emit = function (event, data, source) {
  1034. for (var i =0; i < this.subscriptions.length; i++) {
  1035. var subscription = this.subscriptions[i];
  1036. if (subscription.regexp.test(event)) {
  1037. if (subscription.callback) {
  1038. subscription.callback(event, data, source);
  1039. }
  1040. }
  1041. }
  1042. };
  1043. /**
  1044. * DataSet
  1045. *
  1046. * Usage:
  1047. * var dataSet = new DataSet({
  1048. * fieldId: '_id',
  1049. * convert: {
  1050. * // ...
  1051. * }
  1052. * });
  1053. *
  1054. * dataSet.add(item);
  1055. * dataSet.add(data);
  1056. * dataSet.update(item);
  1057. * dataSet.update(data);
  1058. * dataSet.remove(id);
  1059. * dataSet.remove(ids);
  1060. * var data = dataSet.get();
  1061. * var data = dataSet.get(id);
  1062. * var data = dataSet.get(ids);
  1063. * var data = dataSet.get(ids, options, data);
  1064. * dataSet.clear();
  1065. *
  1066. * A data set can:
  1067. * - add/remove/update data
  1068. * - gives triggers upon changes in the data
  1069. * - can import/export data in various data formats
  1070. *
  1071. * @param {Object} [options] Available options:
  1072. * {String} fieldId Field name of the id in the
  1073. * items, 'id' by default.
  1074. * {Object.<String, String} convert
  1075. * A map with field names as key,
  1076. * and the field type as value.
  1077. * @constructor DataSet
  1078. */
  1079. // TODO: add a DataSet constructor DataSet(data, options)
  1080. function DataSet (options) {
  1081. this.id = util.randomUUID();
  1082. this.options = options || {};
  1083. this.data = {}; // map with data indexed by id
  1084. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1085. this.convert = {}; // field types by field name
  1086. if (this.options.convert) {
  1087. for (var field in this.options.convert) {
  1088. if (this.options.convert.hasOwnProperty(field)) {
  1089. var value = this.options.convert[field];
  1090. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1091. this.convert[field] = 'Date';
  1092. }
  1093. else {
  1094. this.convert[field] = value;
  1095. }
  1096. }
  1097. }
  1098. }
  1099. // event subscribers
  1100. this.subscribers = {};
  1101. this.internalIds = {}; // internally generated id's
  1102. }
  1103. /**
  1104. * Subscribe to an event, add an event listener
  1105. * @param {String} event Event name. Available events: 'put', 'update',
  1106. * 'remove'
  1107. * @param {function} callback Callback method. Called with three parameters:
  1108. * {String} event
  1109. * {Object | null} params
  1110. * {String | Number} senderId
  1111. */
  1112. DataSet.prototype.subscribe = function (event, callback) {
  1113. var subscribers = this.subscribers[event];
  1114. if (!subscribers) {
  1115. subscribers = [];
  1116. this.subscribers[event] = subscribers;
  1117. }
  1118. subscribers.push({
  1119. callback: callback
  1120. });
  1121. };
  1122. /**
  1123. * Unsubscribe from an event, remove an event listener
  1124. * @param {String} event
  1125. * @param {function} callback
  1126. */
  1127. DataSet.prototype.unsubscribe = function (event, callback) {
  1128. var subscribers = this.subscribers[event];
  1129. if (subscribers) {
  1130. this.subscribers[event] = subscribers.filter(function (listener) {
  1131. return (listener.callback != callback);
  1132. });
  1133. }
  1134. };
  1135. /**
  1136. * Trigger an event
  1137. * @param {String} event
  1138. * @param {Object | null} params
  1139. * @param {String} [senderId] Optional id of the sender.
  1140. * @private
  1141. */
  1142. DataSet.prototype._trigger = function (event, params, senderId) {
  1143. if (event == '*') {
  1144. throw new Error('Cannot trigger event *');
  1145. }
  1146. var subscribers = [];
  1147. if (event in this.subscribers) {
  1148. subscribers = subscribers.concat(this.subscribers[event]);
  1149. }
  1150. if ('*' in this.subscribers) {
  1151. subscribers = subscribers.concat(this.subscribers['*']);
  1152. }
  1153. for (var i = 0; i < subscribers.length; i++) {
  1154. var subscriber = subscribers[i];
  1155. if (subscriber.callback) {
  1156. subscriber.callback(event, params, senderId || null);
  1157. }
  1158. }
  1159. };
  1160. /**
  1161. * Add data.
  1162. * Adding an item will fail when there already is an item with the same id.
  1163. * @param {Object | Array | DataTable} data
  1164. * @param {String} [senderId] Optional sender id
  1165. * @return {Array} addedIds Array with the ids of the added items
  1166. */
  1167. DataSet.prototype.add = function (data, senderId) {
  1168. var addedIds = [],
  1169. id,
  1170. me = this;
  1171. if (data instanceof Array) {
  1172. // Array
  1173. for (var i = 0, len = data.length; i < len; i++) {
  1174. id = me._addItem(data[i]);
  1175. addedIds.push(id);
  1176. }
  1177. }
  1178. else if (util.isDataTable(data)) {
  1179. // Google DataTable
  1180. var columns = this._getColumnNames(data);
  1181. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1182. var item = {};
  1183. for (var col = 0, cols = columns.length; col < cols; col++) {
  1184. var field = columns[col];
  1185. item[field] = data.getValue(row, col);
  1186. }
  1187. id = me._addItem(item);
  1188. addedIds.push(id);
  1189. }
  1190. }
  1191. else if (data instanceof Object) {
  1192. // Single item
  1193. id = me._addItem(data);
  1194. addedIds.push(id);
  1195. }
  1196. else {
  1197. throw new Error('Unknown dataType');
  1198. }
  1199. if (addedIds.length) {
  1200. this._trigger('add', {items: addedIds}, senderId);
  1201. }
  1202. return addedIds;
  1203. };
  1204. /**
  1205. * Update existing items. When an item does not exist, it will be created
  1206. * @param {Object | Array | DataTable} data
  1207. * @param {String} [senderId] Optional sender id
  1208. * @return {Array} updatedIds The ids of the added or updated items
  1209. */
  1210. DataSet.prototype.update = function (data, senderId) {
  1211. var addedIds = [],
  1212. updatedIds = [],
  1213. me = this,
  1214. fieldId = me.fieldId;
  1215. var addOrUpdate = function (item) {
  1216. var id = item[fieldId];
  1217. if (me.data[id]) {
  1218. // update item
  1219. id = me._updateItem(item);
  1220. updatedIds.push(id);
  1221. }
  1222. else {
  1223. // add new item
  1224. id = me._addItem(item);
  1225. addedIds.push(id);
  1226. }
  1227. };
  1228. if (data instanceof Array) {
  1229. // Array
  1230. for (var i = 0, len = data.length; i < len; i++) {
  1231. addOrUpdate(data[i]);
  1232. }
  1233. }
  1234. else if (util.isDataTable(data)) {
  1235. // Google DataTable
  1236. var columns = this._getColumnNames(data);
  1237. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1238. var item = {};
  1239. for (var col = 0, cols = columns.length; col < cols; col++) {
  1240. var field = columns[col];
  1241. item[field] = data.getValue(row, col);
  1242. }
  1243. addOrUpdate(item);
  1244. }
  1245. }
  1246. else if (data instanceof Object) {
  1247. // Single item
  1248. addOrUpdate(data);
  1249. }
  1250. else {
  1251. throw new Error('Unknown dataType');
  1252. }
  1253. if (addedIds.length) {
  1254. this._trigger('add', {items: addedIds}, senderId);
  1255. }
  1256. if (updatedIds.length) {
  1257. this._trigger('update', {items: updatedIds}, senderId);
  1258. }
  1259. return addedIds.concat(updatedIds);
  1260. };
  1261. /**
  1262. * Get a data item or multiple items.
  1263. *
  1264. * Usage:
  1265. *
  1266. * get()
  1267. * get(options: Object)
  1268. * get(options: Object, data: Array | DataTable)
  1269. *
  1270. * get(id: Number | String)
  1271. * get(id: Number | String, options: Object)
  1272. * get(id: Number | String, options: Object, data: Array | DataTable)
  1273. *
  1274. * get(ids: Number[] | String[])
  1275. * get(ids: Number[] | String[], options: Object)
  1276. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1277. *
  1278. * Where:
  1279. *
  1280. * {Number | String} id The id of an item
  1281. * {Number[] | String{}} ids An array with ids of items
  1282. * {Object} options An Object with options. Available options:
  1283. * {String} [type] Type of data to be returned. Can
  1284. * be 'DataTable' or 'Array' (default)
  1285. * {Object.<String, String>} [convert]
  1286. * {String[]} [fields] field names to be returned
  1287. * {function} [filter] filter items
  1288. * {String | function} [order] Order the items by
  1289. * a field name or custom sort function.
  1290. * {Array | DataTable} [data] If provided, items will be appended to this
  1291. * array or table. Required in case of Google
  1292. * DataTable.
  1293. *
  1294. * @throws Error
  1295. */
  1296. DataSet.prototype.get = function (args) {
  1297. var me = this;
  1298. // parse the arguments
  1299. var id, ids, options, data;
  1300. var firstType = util.getType(arguments[0]);
  1301. if (firstType == 'String' || firstType == 'Number') {
  1302. // get(id [, options] [, data])
  1303. id = arguments[0];
  1304. options = arguments[1];
  1305. data = arguments[2];
  1306. }
  1307. else if (firstType == 'Array') {
  1308. // get(ids [, options] [, data])
  1309. ids = arguments[0];
  1310. options = arguments[1];
  1311. data = arguments[2];
  1312. }
  1313. else {
  1314. // get([, options] [, data])
  1315. options = arguments[0];
  1316. data = arguments[1];
  1317. }
  1318. // determine the return type
  1319. var type;
  1320. if (options && options.type) {
  1321. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1322. if (data && (type != util.getType(data))) {
  1323. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1324. 'does not correspond with specified options.type (' + options.type + ')');
  1325. }
  1326. if (type == 'DataTable' && !util.isDataTable(data)) {
  1327. throw new Error('Parameter "data" must be a DataTable ' +
  1328. 'when options.type is "DataTable"');
  1329. }
  1330. }
  1331. else if (data) {
  1332. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1333. }
  1334. else {
  1335. type = 'Array';
  1336. }
  1337. // build options
  1338. var convert = options && options.convert || this.options.convert;
  1339. var filter = options && options.filter;
  1340. var items = [], item, itemId, i, len;
  1341. // convert items
  1342. if (id != undefined) {
  1343. // return a single item
  1344. item = me._getItem(id, convert);
  1345. if (filter && !filter(item)) {
  1346. item = null;
  1347. }
  1348. }
  1349. else if (ids != undefined) {
  1350. // return a subset of items
  1351. for (i = 0, len = ids.length; i < len; i++) {
  1352. item = me._getItem(ids[i], convert);
  1353. if (!filter || filter(item)) {
  1354. items.push(item);
  1355. }
  1356. }
  1357. }
  1358. else {
  1359. // return all items
  1360. for (itemId in this.data) {
  1361. if (this.data.hasOwnProperty(itemId)) {
  1362. item = me._getItem(itemId, convert);
  1363. if (!filter || filter(item)) {
  1364. items.push(item);
  1365. }
  1366. }
  1367. }
  1368. }
  1369. // order the results
  1370. if (options && options.order && id == undefined) {
  1371. this._sort(items, options.order);
  1372. }
  1373. // filter fields of the items
  1374. if (options && options.fields) {
  1375. var fields = options.fields;
  1376. if (id != undefined) {
  1377. item = this._filterFields(item, fields);
  1378. }
  1379. else {
  1380. for (i = 0, len = items.length; i < len; i++) {
  1381. items[i] = this._filterFields(items[i], fields);
  1382. }
  1383. }
  1384. }
  1385. // return the results
  1386. if (type == 'DataTable') {
  1387. var columns = this._getColumnNames(data);
  1388. if (id != undefined) {
  1389. // append a single item to the data table
  1390. me._appendRow(data, columns, item);
  1391. }
  1392. else {
  1393. // copy the items to the provided data table
  1394. for (i = 0, len = items.length; i < len; i++) {
  1395. me._appendRow(data, columns, items[i]);
  1396. }
  1397. }
  1398. return data;
  1399. }
  1400. else {
  1401. // return an array
  1402. if (id != undefined) {
  1403. // a single item
  1404. return item;
  1405. }
  1406. else {
  1407. // multiple items
  1408. if (data) {
  1409. // copy the items to the provided array
  1410. for (i = 0, len = items.length; i < len; i++) {
  1411. data.push(items[i]);
  1412. }
  1413. return data;
  1414. }
  1415. else {
  1416. // just return our array
  1417. return items;
  1418. }
  1419. }
  1420. }
  1421. };
  1422. /**
  1423. * Get ids of all items or from a filtered set of items.
  1424. * @param {Object} [options] An Object with options. Available options:
  1425. * {function} [filter] filter items
  1426. * {String | function} [order] Order the items by
  1427. * a field name or custom sort function.
  1428. * @return {Array} ids
  1429. */
  1430. DataSet.prototype.getIds = function (options) {
  1431. var data = this.data,
  1432. filter = options && options.filter,
  1433. order = options && options.order,
  1434. convert = options && options.convert || this.options.convert,
  1435. i,
  1436. len,
  1437. id,
  1438. item,
  1439. items,
  1440. ids = [];
  1441. if (filter) {
  1442. // get filtered items
  1443. if (order) {
  1444. // create ordered list
  1445. items = [];
  1446. for (id in data) {
  1447. if (data.hasOwnProperty(id)) {
  1448. item = this._getItem(id, convert);
  1449. if (filter(item)) {
  1450. items.push(item);
  1451. }
  1452. }
  1453. }
  1454. this._sort(items, order);
  1455. for (i = 0, len = items.length; i < len; i++) {
  1456. ids[i] = items[i][this.fieldId];
  1457. }
  1458. }
  1459. else {
  1460. // create unordered list
  1461. for (id in data) {
  1462. if (data.hasOwnProperty(id)) {
  1463. item = this._getItem(id, convert);
  1464. if (filter(item)) {
  1465. ids.push(item[this.fieldId]);
  1466. }
  1467. }
  1468. }
  1469. }
  1470. }
  1471. else {
  1472. // get all items
  1473. if (order) {
  1474. // create an ordered list
  1475. items = [];
  1476. for (id in data) {
  1477. if (data.hasOwnProperty(id)) {
  1478. items.push(data[id]);
  1479. }
  1480. }
  1481. this._sort(items, order);
  1482. for (i = 0, len = items.length; i < len; i++) {
  1483. ids[i] = items[i][this.fieldId];
  1484. }
  1485. }
  1486. else {
  1487. // create unordered list
  1488. for (id in data) {
  1489. if (data.hasOwnProperty(id)) {
  1490. item = data[id];
  1491. ids.push(item[this.fieldId]);
  1492. }
  1493. }
  1494. }
  1495. }
  1496. return ids;
  1497. };
  1498. /**
  1499. * Execute a callback function for every item in the dataset.
  1500. * The order of the items is not determined.
  1501. * @param {function} callback
  1502. * @param {Object} [options] Available options:
  1503. * {Object.<String, String>} [convert]
  1504. * {String[]} [fields] filter fields
  1505. * {function} [filter] filter items
  1506. * {String | function} [order] Order the items by
  1507. * a field name or custom sort function.
  1508. */
  1509. DataSet.prototype.forEach = function (callback, options) {
  1510. var filter = options && options.filter,
  1511. convert = options && options.convert || this.options.convert,
  1512. data = this.data,
  1513. item,
  1514. id;
  1515. if (options && options.order) {
  1516. // execute forEach on ordered list
  1517. var items = this.get(options);
  1518. for (var i = 0, len = items.length; i < len; i++) {
  1519. item = items[i];
  1520. id = item[this.fieldId];
  1521. callback(item, id);
  1522. }
  1523. }
  1524. else {
  1525. // unordered
  1526. for (id in data) {
  1527. if (data.hasOwnProperty(id)) {
  1528. item = this._getItem(id, convert);
  1529. if (!filter || filter(item)) {
  1530. callback(item, id);
  1531. }
  1532. }
  1533. }
  1534. }
  1535. };
  1536. /**
  1537. * Map every item in the dataset.
  1538. * @param {function} callback
  1539. * @param {Object} [options] Available options:
  1540. * {Object.<String, String>} [convert]
  1541. * {String[]} [fields] filter fields
  1542. * {function} [filter] filter items
  1543. * {String | function} [order] Order the items by
  1544. * a field name or custom sort function.
  1545. * @return {Object[]} mappedItems
  1546. */
  1547. DataSet.prototype.map = function (callback, options) {
  1548. var filter = options && options.filter,
  1549. convert = options && options.convert || this.options.convert,
  1550. mappedItems = [],
  1551. data = this.data,
  1552. item;
  1553. // convert and filter items
  1554. for (var id in data) {
  1555. if (data.hasOwnProperty(id)) {
  1556. item = this._getItem(id, convert);
  1557. if (!filter || filter(item)) {
  1558. mappedItems.push(callback(item, id));
  1559. }
  1560. }
  1561. }
  1562. // order items
  1563. if (options && options.order) {
  1564. this._sort(mappedItems, options.order);
  1565. }
  1566. return mappedItems;
  1567. };
  1568. /**
  1569. * Filter the fields of an item
  1570. * @param {Object} item
  1571. * @param {String[]} fields Field names
  1572. * @return {Object} filteredItem
  1573. * @private
  1574. */
  1575. DataSet.prototype._filterFields = function (item, fields) {
  1576. var filteredItem = {};
  1577. for (var field in item) {
  1578. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1579. filteredItem[field] = item[field];
  1580. }
  1581. }
  1582. return filteredItem;
  1583. };
  1584. /**
  1585. * Sort the provided array with items
  1586. * @param {Object[]} items
  1587. * @param {String | function} order A field name or custom sort function.
  1588. * @private
  1589. */
  1590. DataSet.prototype._sort = function (items, order) {
  1591. if (util.isString(order)) {
  1592. // order by provided field name
  1593. var name = order; // field name
  1594. items.sort(function (a, b) {
  1595. var av = a[name];
  1596. var bv = b[name];
  1597. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1598. });
  1599. }
  1600. else if (typeof order === 'function') {
  1601. // order by sort function
  1602. items.sort(order);
  1603. }
  1604. // TODO: extend order by an Object {field:String, direction:String}
  1605. // where direction can be 'asc' or 'desc'
  1606. else {
  1607. throw new TypeError('Order must be a function or a string');
  1608. }
  1609. };
  1610. /**
  1611. * Remove an object by pointer or by id
  1612. * @param {String | Number | Object | Array} id Object or id, or an array with
  1613. * objects or ids to be removed
  1614. * @param {String} [senderId] Optional sender id
  1615. * @return {Array} removedIds
  1616. */
  1617. DataSet.prototype.remove = function (id, senderId) {
  1618. var removedIds = [],
  1619. i, len, removedId;
  1620. if (id instanceof Array) {
  1621. for (i = 0, len = id.length; i < len; i++) {
  1622. removedId = this._remove(id[i]);
  1623. if (removedId != null) {
  1624. removedIds.push(removedId);
  1625. }
  1626. }
  1627. }
  1628. else {
  1629. removedId = this._remove(id);
  1630. if (removedId != null) {
  1631. removedIds.push(removedId);
  1632. }
  1633. }
  1634. if (removedIds.length) {
  1635. this._trigger('remove', {items: removedIds}, senderId);
  1636. }
  1637. return removedIds;
  1638. };
  1639. /**
  1640. * Remove an item by its id
  1641. * @param {Number | String | Object} id id or item
  1642. * @returns {Number | String | null} id
  1643. * @private
  1644. */
  1645. DataSet.prototype._remove = function (id) {
  1646. if (util.isNumber(id) || util.isString(id)) {
  1647. if (this.data[id]) {
  1648. delete this.data[id];
  1649. delete this.internalIds[id];
  1650. return id;
  1651. }
  1652. }
  1653. else if (id instanceof Object) {
  1654. var itemId = id[this.fieldId];
  1655. if (itemId && this.data[itemId]) {
  1656. delete this.data[itemId];
  1657. delete this.internalIds[itemId];
  1658. return itemId;
  1659. }
  1660. }
  1661. return null;
  1662. };
  1663. /**
  1664. * Clear the data
  1665. * @param {String} [senderId] Optional sender id
  1666. * @return {Array} removedIds The ids of all removed items
  1667. */
  1668. DataSet.prototype.clear = function (senderId) {
  1669. var ids = Object.keys(this.data);
  1670. this.data = {};
  1671. this.internalIds = {};
  1672. this._trigger('remove', {items: ids}, senderId);
  1673. return ids;
  1674. };
  1675. /**
  1676. * Find the item with maximum value of a specified field
  1677. * @param {String} field
  1678. * @return {Object | null} item Item containing max value, or null if no items
  1679. */
  1680. DataSet.prototype.max = function (field) {
  1681. var data = this.data,
  1682. max = null,
  1683. maxField = null;
  1684. for (var id in data) {
  1685. if (data.hasOwnProperty(id)) {
  1686. var item = data[id];
  1687. var itemField = item[field];
  1688. if (itemField != null && (!max || itemField > maxField)) {
  1689. max = item;
  1690. maxField = itemField;
  1691. }
  1692. }
  1693. }
  1694. return max;
  1695. };
  1696. /**
  1697. * Find the item with minimum value of a specified field
  1698. * @param {String} field
  1699. * @return {Object | null} item Item containing max value, or null if no items
  1700. */
  1701. DataSet.prototype.min = function (field) {
  1702. var data = this.data,
  1703. min = null,
  1704. minField = null;
  1705. for (var id in data) {
  1706. if (data.hasOwnProperty(id)) {
  1707. var item = data[id];
  1708. var itemField = item[field];
  1709. if (itemField != null && (!min || itemField < minField)) {
  1710. min = item;
  1711. minField = itemField;
  1712. }
  1713. }
  1714. }
  1715. return min;
  1716. };
  1717. /**
  1718. * Find all distinct values of a specified field
  1719. * @param {String} field
  1720. * @return {Array} values Array containing all distinct values. If the data
  1721. * items do not contain the specified field, an array
  1722. * containing a single value undefined is returned.
  1723. * The returned array is unordered.
  1724. */
  1725. DataSet.prototype.distinct = function (field) {
  1726. var data = this.data,
  1727. values = [],
  1728. fieldType = this.options.convert[field],
  1729. count = 0;
  1730. for (var prop in data) {
  1731. if (data.hasOwnProperty(prop)) {
  1732. var item = data[prop];
  1733. var value = util.convert(item[field], fieldType);
  1734. var exists = false;
  1735. for (var i = 0; i < count; i++) {
  1736. if (values[i] == value) {
  1737. exists = true;
  1738. break;
  1739. }
  1740. }
  1741. if (!exists) {
  1742. values[count] = value;
  1743. count++;
  1744. }
  1745. }
  1746. }
  1747. return values;
  1748. };
  1749. /**
  1750. * Add a single item. Will fail when an item with the same id already exists.
  1751. * @param {Object} item
  1752. * @return {String} id
  1753. * @private
  1754. */
  1755. DataSet.prototype._addItem = function (item) {
  1756. var id = item[this.fieldId];
  1757. if (id != undefined) {
  1758. // check whether this id is already taken
  1759. if (this.data[id]) {
  1760. // item already exists
  1761. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1762. }
  1763. }
  1764. else {
  1765. // generate an id
  1766. id = util.randomUUID();
  1767. item[this.fieldId] = id;
  1768. this.internalIds[id] = item;
  1769. }
  1770. var d = {};
  1771. for (var field in item) {
  1772. if (item.hasOwnProperty(field)) {
  1773. var fieldType = this.convert[field]; // type may be undefined
  1774. d[field] = util.convert(item[field], fieldType);
  1775. }
  1776. }
  1777. this.data[id] = d;
  1778. return id;
  1779. };
  1780. /**
  1781. * Get an item. Fields can be converted to a specific type
  1782. * @param {String} id
  1783. * @param {Object.<String, String>} [convert] field types to convert
  1784. * @return {Object | null} item
  1785. * @private
  1786. */
  1787. DataSet.prototype._getItem = function (id, convert) {
  1788. var field, value;
  1789. // get the item from the dataset
  1790. var raw = this.data[id];
  1791. if (!raw) {
  1792. return null;
  1793. }
  1794. // convert the items field types
  1795. var converted = {},
  1796. fieldId = this.fieldId,
  1797. internalIds = this.internalIds;
  1798. if (convert) {
  1799. for (field in raw) {
  1800. if (raw.hasOwnProperty(field)) {
  1801. value = raw[field];
  1802. // output all fields, except internal ids
  1803. if ((field != fieldId) || !(value in internalIds)) {
  1804. converted[field] = util.convert(value, convert[field]);
  1805. }
  1806. }
  1807. }
  1808. }
  1809. else {
  1810. // no field types specified, no converting needed
  1811. for (field in raw) {
  1812. if (raw.hasOwnProperty(field)) {
  1813. value = raw[field];
  1814. // output all fields, except internal ids
  1815. if ((field != fieldId) || !(value in internalIds)) {
  1816. converted[field] = value;
  1817. }
  1818. }
  1819. }
  1820. }
  1821. return converted;
  1822. };
  1823. /**
  1824. * Update a single item: merge with existing item.
  1825. * Will fail when the item has no id, or when there does not exist an item
  1826. * with the same id.
  1827. * @param {Object} item
  1828. * @return {String} id
  1829. * @private
  1830. */
  1831. DataSet.prototype._updateItem = function (item) {
  1832. var id = item[this.fieldId];
  1833. if (id == undefined) {
  1834. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1835. }
  1836. var d = this.data[id];
  1837. if (!d) {
  1838. // item doesn't exist
  1839. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1840. }
  1841. // merge with current item
  1842. for (var field in item) {
  1843. if (item.hasOwnProperty(field)) {
  1844. var fieldType = this.convert[field]; // type may be undefined
  1845. d[field] = util.convert(item[field], fieldType);
  1846. }
  1847. }
  1848. return id;
  1849. };
  1850. /**
  1851. * Get an array with the column names of a Google DataTable
  1852. * @param {DataTable} dataTable
  1853. * @return {String[]} columnNames
  1854. * @private
  1855. */
  1856. DataSet.prototype._getColumnNames = function (dataTable) {
  1857. var columns = [];
  1858. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1859. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1860. }
  1861. return columns;
  1862. };
  1863. /**
  1864. * Append an item as a row to the dataTable
  1865. * @param dataTable
  1866. * @param columns
  1867. * @param item
  1868. * @private
  1869. */
  1870. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1871. var row = dataTable.addRow();
  1872. for (var col = 0, cols = columns.length; col < cols; col++) {
  1873. var field = columns[col];
  1874. dataTable.setValue(row, col, item[field]);
  1875. }
  1876. };
  1877. /**
  1878. * DataView
  1879. *
  1880. * a dataview offers a filtered view on a dataset or an other dataview.
  1881. *
  1882. * @param {DataSet | DataView} data
  1883. * @param {Object} [options] Available options: see method get
  1884. *
  1885. * @constructor DataView
  1886. */
  1887. function DataView (data, options) {
  1888. this.id = util.randomUUID();
  1889. this.data = null;
  1890. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1891. this.options = options || {};
  1892. this.fieldId = 'id'; // name of the field containing id
  1893. this.subscribers = {}; // event subscribers
  1894. var me = this;
  1895. this.listener = function () {
  1896. me._onEvent.apply(me, arguments);
  1897. };
  1898. this.setData(data);
  1899. }
  1900. // TODO: implement a function .config() to dynamically update things like configured filter
  1901. // and trigger changes accordingly
  1902. /**
  1903. * Set a data source for the view
  1904. * @param {DataSet | DataView} data
  1905. */
  1906. DataView.prototype.setData = function (data) {
  1907. var ids, dataItems, i, len;
  1908. if (this.data) {
  1909. // unsubscribe from current dataset
  1910. if (this.data.unsubscribe) {
  1911. this.data.unsubscribe('*', this.listener);
  1912. }
  1913. // trigger a remove of all items in memory
  1914. ids = [];
  1915. for (var id in this.ids) {
  1916. if (this.ids.hasOwnProperty(id)) {
  1917. ids.push(id);
  1918. }
  1919. }
  1920. this.ids = {};
  1921. this._trigger('remove', {items: ids});
  1922. }
  1923. this.data = data;
  1924. if (this.data) {
  1925. // update fieldId
  1926. this.fieldId = this.options.fieldId ||
  1927. (this.data && this.data.options && this.data.options.fieldId) ||
  1928. 'id';
  1929. // trigger an add of all added items
  1930. ids = this.data.getIds({filter: this.options && this.options.filter});
  1931. for (i = 0, len = ids.length; i < len; i++) {
  1932. id = ids[i];
  1933. this.ids[id] = true;
  1934. }
  1935. this._trigger('add', {items: ids});
  1936. // subscribe to new dataset
  1937. if (this.data.subscribe) {
  1938. this.data.subscribe('*', this.listener);
  1939. }
  1940. }
  1941. };
  1942. /**
  1943. * Get data from the data view
  1944. *
  1945. * Usage:
  1946. *
  1947. * get()
  1948. * get(options: Object)
  1949. * get(options: Object, data: Array | DataTable)
  1950. *
  1951. * get(id: Number)
  1952. * get(id: Number, options: Object)
  1953. * get(id: Number, options: Object, data: Array | DataTable)
  1954. *
  1955. * get(ids: Number[])
  1956. * get(ids: Number[], options: Object)
  1957. * get(ids: Number[], options: Object, data: Array | DataTable)
  1958. *
  1959. * Where:
  1960. *
  1961. * {Number | String} id The id of an item
  1962. * {Number[] | String{}} ids An array with ids of items
  1963. * {Object} options An Object with options. Available options:
  1964. * {String} [type] Type of data to be returned. Can
  1965. * be 'DataTable' or 'Array' (default)
  1966. * {Object.<String, String>} [convert]
  1967. * {String[]} [fields] field names to be returned
  1968. * {function} [filter] filter items
  1969. * {String | function} [order] Order the items by
  1970. * a field name or custom sort function.
  1971. * {Array | DataTable} [data] If provided, items will be appended to this
  1972. * array or table. Required in case of Google
  1973. * DataTable.
  1974. * @param args
  1975. */
  1976. DataView.prototype.get = function (args) {
  1977. var me = this;
  1978. // parse the arguments
  1979. var ids, options, data;
  1980. var firstType = util.getType(arguments[0]);
  1981. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1982. // get(id(s) [, options] [, data])
  1983. ids = arguments[0]; // can be a single id or an array with ids
  1984. options = arguments[1];
  1985. data = arguments[2];
  1986. }
  1987. else {
  1988. // get([, options] [, data])
  1989. options = arguments[0];
  1990. data = arguments[1];
  1991. }
  1992. // extend the options with the default options and provided options
  1993. var viewOptions = util.extend({}, this.options, options);
  1994. // create a combined filter method when needed
  1995. if (this.options.filter && options && options.filter) {
  1996. viewOptions.filter = function (item) {
  1997. return me.options.filter(item) && options.filter(item);
  1998. }
  1999. }
  2000. // build up the call to the linked data set
  2001. var getArguments = [];
  2002. if (ids != undefined) {
  2003. getArguments.push(ids);
  2004. }
  2005. getArguments.push(viewOptions);
  2006. getArguments.push(data);
  2007. return this.data && this.data.get.apply(this.data, getArguments);
  2008. };
  2009. /**
  2010. * Get ids of all items or from a filtered set of items.
  2011. * @param {Object} [options] An Object with options. Available options:
  2012. * {function} [filter] filter items
  2013. * {String | function} [order] Order the items by
  2014. * a field name or custom sort function.
  2015. * @return {Array} ids
  2016. */
  2017. DataView.prototype.getIds = function (options) {
  2018. var ids;
  2019. if (this.data) {
  2020. var defaultFilter = this.options.filter;
  2021. var filter;
  2022. if (options && options.filter) {
  2023. if (defaultFilter) {
  2024. filter = function (item) {
  2025. return defaultFilter(item) && options.filter(item);
  2026. }
  2027. }
  2028. else {
  2029. filter = options.filter;
  2030. }
  2031. }
  2032. else {
  2033. filter = defaultFilter;
  2034. }
  2035. ids = this.data.getIds({
  2036. filter: filter,
  2037. order: options && options.order
  2038. });
  2039. }
  2040. else {
  2041. ids = [];
  2042. }
  2043. return ids;
  2044. };
  2045. /**
  2046. * Event listener. Will propagate all events from the connected data set to
  2047. * the subscribers of the DataView, but will filter the items and only trigger
  2048. * when there are changes in the filtered data set.
  2049. * @param {String} event
  2050. * @param {Object | null} params
  2051. * @param {String} senderId
  2052. * @private
  2053. */
  2054. DataView.prototype._onEvent = function (event, params, senderId) {
  2055. var i, len, id, item,
  2056. ids = params && params.items,
  2057. data = this.data,
  2058. added = [],
  2059. updated = [],
  2060. removed = [];
  2061. if (ids && data) {
  2062. switch (event) {
  2063. case 'add':
  2064. // filter the ids of the added items
  2065. for (i = 0, len = ids.length; i < len; i++) {
  2066. id = ids[i];
  2067. item = this.get(id);
  2068. if (item) {
  2069. this.ids[id] = true;
  2070. added.push(id);
  2071. }
  2072. }
  2073. break;
  2074. case 'update':
  2075. // determine the event from the views viewpoint: an updated
  2076. // item can be added, updated, or removed from this view.
  2077. for (i = 0, len = ids.length; i < len; i++) {
  2078. id = ids[i];
  2079. item = this.get(id);
  2080. if (item) {
  2081. if (this.ids[id]) {
  2082. updated.push(id);
  2083. }
  2084. else {
  2085. this.ids[id] = true;
  2086. added.push(id);
  2087. }
  2088. }
  2089. else {
  2090. if (this.ids[id]) {
  2091. delete this.ids[id];
  2092. removed.push(id);
  2093. }
  2094. else {
  2095. // nothing interesting for me :-(
  2096. }
  2097. }
  2098. }
  2099. break;
  2100. case 'remove':
  2101. // filter the ids of the removed items
  2102. for (i = 0, len = ids.length; i < len; i++) {
  2103. id = ids[i];
  2104. if (this.ids[id]) {
  2105. delete this.ids[id];
  2106. removed.push(id);
  2107. }
  2108. }
  2109. break;
  2110. }
  2111. if (added.length) {
  2112. this._trigger('add', {items: added}, senderId);
  2113. }
  2114. if (updated.length) {
  2115. this._trigger('update', {items: updated}, senderId);
  2116. }
  2117. if (removed.length) {
  2118. this._trigger('remove', {items: removed}, senderId);
  2119. }
  2120. }
  2121. };
  2122. // copy subscription functionality from DataSet
  2123. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2124. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2125. DataView.prototype._trigger = DataSet.prototype._trigger;
  2126. /**
  2127. * @constructor TimeStep
  2128. * The class TimeStep is an iterator for dates. You provide a start date and an
  2129. * end date. The class itself determines the best scale (step size) based on the
  2130. * provided start Date, end Date, and minimumStep.
  2131. *
  2132. * If minimumStep is provided, the step size is chosen as close as possible
  2133. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2134. * provided, the scale is set to 1 DAY.
  2135. * The minimumStep should correspond with the onscreen size of about 6 characters
  2136. *
  2137. * Alternatively, you can set a scale by hand.
  2138. * After creation, you can initialize the class by executing first(). Then you
  2139. * can iterate from the start date to the end date via next(). You can check if
  2140. * the end date is reached with the function hasNext(). After each step, you can
  2141. * retrieve the current date via getCurrent().
  2142. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2143. * days, to years.
  2144. *
  2145. * Version: 1.2
  2146. *
  2147. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2148. * or new Date(2010, 9, 21, 23, 45, 00)
  2149. * @param {Date} [end] The end date
  2150. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2151. */
  2152. TimeStep = function(start, end, minimumStep) {
  2153. // variables
  2154. this.current = new Date();
  2155. this._start = new Date();
  2156. this._end = new Date();
  2157. this.autoScale = true;
  2158. this.scale = TimeStep.SCALE.DAY;
  2159. this.step = 1;
  2160. // initialize the range
  2161. this.setRange(start, end, minimumStep);
  2162. };
  2163. /// enum scale
  2164. TimeStep.SCALE = {
  2165. MILLISECOND: 1,
  2166. SECOND: 2,
  2167. MINUTE: 3,
  2168. HOUR: 4,
  2169. DAY: 5,
  2170. WEEKDAY: 6,
  2171. MONTH: 7,
  2172. YEAR: 8
  2173. };
  2174. /**
  2175. * Set a new range
  2176. * If minimumStep is provided, the step size is chosen as close as possible
  2177. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2178. * provided, the scale is set to 1 DAY.
  2179. * The minimumStep should correspond with the onscreen size of about 6 characters
  2180. * @param {Date} [start] The start date and time.
  2181. * @param {Date} [end] The end date and time.
  2182. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2183. */
  2184. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2185. if (!(start instanceof Date) || !(end instanceof Date)) {
  2186. throw "No legal start or end date in method setRange";
  2187. }
  2188. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2189. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2190. if (this.autoScale) {
  2191. this.setMinimumStep(minimumStep);
  2192. }
  2193. };
  2194. /**
  2195. * Set the range iterator to the start date.
  2196. */
  2197. TimeStep.prototype.first = function() {
  2198. this.current = new Date(this._start.valueOf());
  2199. this.roundToMinor();
  2200. };
  2201. /**
  2202. * Round the current date to the first minor date value
  2203. * This must be executed once when the current date is set to start Date
  2204. */
  2205. TimeStep.prototype.roundToMinor = function() {
  2206. // round to floor
  2207. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2208. //noinspection FallthroughInSwitchStatementJS
  2209. switch (this.scale) {
  2210. case TimeStep.SCALE.YEAR:
  2211. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2212. this.current.setMonth(0);
  2213. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2214. case TimeStep.SCALE.DAY: // intentional fall through
  2215. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2216. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2217. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2218. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2219. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2220. }
  2221. if (this.step != 1) {
  2222. // round down to the first minor value that is a multiple of the current step size
  2223. switch (this.scale) {
  2224. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2225. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2226. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2227. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2228. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2229. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2230. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2231. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2232. default: break;
  2233. }
  2234. }
  2235. };
  2236. /**
  2237. * Check if the there is a next step
  2238. * @return {boolean} true if the current date has not passed the end date
  2239. */
  2240. TimeStep.prototype.hasNext = function () {
  2241. return (this.current.valueOf() <= this._end.valueOf());
  2242. };
  2243. /**
  2244. * Do the next step
  2245. */
  2246. TimeStep.prototype.next = function() {
  2247. var prev = this.current.valueOf();
  2248. // Two cases, needed to prevent issues with switching daylight savings
  2249. // (end of March and end of October)
  2250. if (this.current.getMonth() < 6) {
  2251. switch (this.scale) {
  2252. case TimeStep.SCALE.MILLISECOND:
  2253. this.current = new Date(this.current.valueOf() + this.step); break;
  2254. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2255. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2256. case TimeStep.SCALE.HOUR:
  2257. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2258. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2259. var h = this.current.getHours();
  2260. this.current.setHours(h - (h % this.step));
  2261. break;
  2262. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2263. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2264. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2265. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2266. default: break;
  2267. }
  2268. }
  2269. else {
  2270. switch (this.scale) {
  2271. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2272. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2273. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2274. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2275. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2276. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2277. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2278. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2279. default: break;
  2280. }
  2281. }
  2282. if (this.step != 1) {
  2283. // round down to the correct major value
  2284. switch (this.scale) {
  2285. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2286. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2287. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2288. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2289. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2290. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2291. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2292. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2293. default: break;
  2294. }
  2295. }
  2296. // safety mechanism: if current time is still unchanged, move to the end
  2297. if (this.current.valueOf() == prev) {
  2298. this.current = new Date(this._end.valueOf());
  2299. }
  2300. };
  2301. /**
  2302. * Get the current datetime
  2303. * @return {Date} current The current date
  2304. */
  2305. TimeStep.prototype.getCurrent = function() {
  2306. return this.current;
  2307. };
  2308. /**
  2309. * Set a custom scale. Autoscaling will be disabled.
  2310. * For example setScale(SCALE.MINUTES, 5) will result
  2311. * in minor steps of 5 minutes, and major steps of an hour.
  2312. *
  2313. * @param {TimeStep.SCALE} newScale
  2314. * A scale. Choose from SCALE.MILLISECOND,
  2315. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2316. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2317. * SCALE.YEAR.
  2318. * @param {Number} newStep A step size, by default 1. Choose for
  2319. * example 1, 2, 5, or 10.
  2320. */
  2321. TimeStep.prototype.setScale = function(newScale, newStep) {
  2322. this.scale = newScale;
  2323. if (newStep > 0) {
  2324. this.step = newStep;
  2325. }
  2326. this.autoScale = false;
  2327. };
  2328. /**
  2329. * Enable or disable autoscaling
  2330. * @param {boolean} enable If true, autoascaling is set true
  2331. */
  2332. TimeStep.prototype.setAutoScale = function (enable) {
  2333. this.autoScale = enable;
  2334. };
  2335. /**
  2336. * Automatically determine the scale that bests fits the provided minimum step
  2337. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2338. */
  2339. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2340. if (minimumStep == undefined) {
  2341. return;
  2342. }
  2343. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2344. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2345. var stepDay = (1000 * 60 * 60 * 24);
  2346. var stepHour = (1000 * 60 * 60);
  2347. var stepMinute = (1000 * 60);
  2348. var stepSecond = (1000);
  2349. var stepMillisecond= (1);
  2350. // find the smallest step that is larger than the provided minimumStep
  2351. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2352. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2353. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2354. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2355. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2356. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2357. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2358. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2359. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2360. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2361. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2362. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2363. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2364. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2365. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2366. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2367. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2368. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2369. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2370. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2371. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2372. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2373. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2374. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2375. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2376. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2377. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2378. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2379. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2380. };
  2381. /**
  2382. * Snap a date to a rounded value. The snap intervals are dependent on the
  2383. * current scale and step.
  2384. * @param {Date} date the date to be snapped
  2385. */
  2386. TimeStep.prototype.snap = function(date) {
  2387. if (this.scale == TimeStep.SCALE.YEAR) {
  2388. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2389. date.setFullYear(Math.round(year / this.step) * this.step);
  2390. date.setMonth(0);
  2391. date.setDate(0);
  2392. date.setHours(0);
  2393. date.setMinutes(0);
  2394. date.setSeconds(0);
  2395. date.setMilliseconds(0);
  2396. }
  2397. else if (this.scale == TimeStep.SCALE.MONTH) {
  2398. if (date.getDate() > 15) {
  2399. date.setDate(1);
  2400. date.setMonth(date.getMonth() + 1);
  2401. // important: first set Date to 1, after that change the month.
  2402. }
  2403. else {
  2404. date.setDate(1);
  2405. }
  2406. date.setHours(0);
  2407. date.setMinutes(0);
  2408. date.setSeconds(0);
  2409. date.setMilliseconds(0);
  2410. }
  2411. else if (this.scale == TimeStep.SCALE.DAY ||
  2412. this.scale == TimeStep.SCALE.WEEKDAY) {
  2413. //noinspection FallthroughInSwitchStatementJS
  2414. switch (this.step) {
  2415. case 5:
  2416. case 2:
  2417. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2418. default:
  2419. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2420. }
  2421. date.setMinutes(0);
  2422. date.setSeconds(0);
  2423. date.setMilliseconds(0);
  2424. }
  2425. else if (this.scale == TimeStep.SCALE.HOUR) {
  2426. switch (this.step) {
  2427. case 4:
  2428. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2429. default:
  2430. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2431. }
  2432. date.setSeconds(0);
  2433. date.setMilliseconds(0);
  2434. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2435. //noinspection FallthroughInSwitchStatementJS
  2436. switch (this.step) {
  2437. case 15:
  2438. case 10:
  2439. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2440. date.setSeconds(0);
  2441. break;
  2442. case 5:
  2443. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2444. default:
  2445. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2446. }
  2447. date.setMilliseconds(0);
  2448. }
  2449. else if (this.scale == TimeStep.SCALE.SECOND) {
  2450. //noinspection FallthroughInSwitchStatementJS
  2451. switch (this.step) {
  2452. case 15:
  2453. case 10:
  2454. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2455. date.setMilliseconds(0);
  2456. break;
  2457. case 5:
  2458. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2459. default:
  2460. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2461. }
  2462. }
  2463. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2464. var step = this.step > 5 ? this.step / 2 : 1;
  2465. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2466. }
  2467. };
  2468. /**
  2469. * Check if the current value is a major value (for example when the step
  2470. * is DAY, a major value is each first day of the MONTH)
  2471. * @return {boolean} true if current date is major, else false.
  2472. */
  2473. TimeStep.prototype.isMajor = function() {
  2474. switch (this.scale) {
  2475. case TimeStep.SCALE.MILLISECOND:
  2476. return (this.current.getMilliseconds() == 0);
  2477. case TimeStep.SCALE.SECOND:
  2478. return (this.current.getSeconds() == 0);
  2479. case TimeStep.SCALE.MINUTE:
  2480. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2481. // Note: this is no bug. Major label is equal for both minute and hour scale
  2482. case TimeStep.SCALE.HOUR:
  2483. return (this.current.getHours() == 0);
  2484. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2485. case TimeStep.SCALE.DAY:
  2486. return (this.current.getDate() == 1);
  2487. case TimeStep.SCALE.MONTH:
  2488. return (this.current.getMonth() == 0);
  2489. case TimeStep.SCALE.YEAR:
  2490. return false;
  2491. default:
  2492. return false;
  2493. }
  2494. };
  2495. /**
  2496. * Returns formatted text for the minor axislabel, depending on the current
  2497. * date and the scale. For example when scale is MINUTE, the current time is
  2498. * formatted as "hh:mm".
  2499. * @param {Date} [date] custom date. if not provided, current date is taken
  2500. */
  2501. TimeStep.prototype.getLabelMinor = function(date) {
  2502. if (date == undefined) {
  2503. date = this.current;
  2504. }
  2505. switch (this.scale) {
  2506. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2507. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2508. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2509. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2510. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2511. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2512. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2513. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2514. default: return '';
  2515. }
  2516. };
  2517. /**
  2518. * Returns formatted text for the major axis label, depending on the current
  2519. * date and the scale. For example when scale is MINUTE, the major scale is
  2520. * hours, and the hour will be formatted as "hh".
  2521. * @param {Date} [date] custom date. if not provided, current date is taken
  2522. */
  2523. TimeStep.prototype.getLabelMajor = function(date) {
  2524. if (date == undefined) {
  2525. date = this.current;
  2526. }
  2527. //noinspection FallthroughInSwitchStatementJS
  2528. switch (this.scale) {
  2529. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2530. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2531. case TimeStep.SCALE.MINUTE:
  2532. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2533. case TimeStep.SCALE.WEEKDAY:
  2534. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2535. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2536. case TimeStep.SCALE.YEAR: return '';
  2537. default: return '';
  2538. }
  2539. };
  2540. /**
  2541. * @constructor Stack
  2542. * Stacks items on top of each other.
  2543. * @param {ItemSet} parent
  2544. * @param {Object} [options]
  2545. */
  2546. function Stack (parent, options) {
  2547. this.parent = parent;
  2548. this.options = options || {};
  2549. this.defaultOptions = {
  2550. order: function (a, b) {
  2551. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2552. // Order: ranges over non-ranges, ranged ordered by width, and
  2553. // lastly ordered by start.
  2554. if (a instanceof ItemRange) {
  2555. if (b instanceof ItemRange) {
  2556. var aInt = (a.data.end - a.data.start);
  2557. var bInt = (b.data.end - b.data.start);
  2558. return (aInt - bInt) || (a.data.start - b.data.start);
  2559. }
  2560. else {
  2561. return -1;
  2562. }
  2563. }
  2564. else {
  2565. if (b instanceof ItemRange) {
  2566. return 1;
  2567. }
  2568. else {
  2569. return (a.data.start - b.data.start);
  2570. }
  2571. }
  2572. },
  2573. margin: {
  2574. item: 10
  2575. }
  2576. };
  2577. this.ordered = []; // ordered items
  2578. }
  2579. /**
  2580. * Set options for the stack
  2581. * @param {Object} options Available options:
  2582. * {ItemSet} parent
  2583. * {Number} margin
  2584. * {function} order Stacking order
  2585. */
  2586. Stack.prototype.setOptions = function setOptions (options) {
  2587. util.extend(this.options, options);
  2588. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2589. };
  2590. /**
  2591. * Stack the items such that they don't overlap. The items will have a minimal
  2592. * distance equal to options.margin.item.
  2593. */
  2594. Stack.prototype.update = function update() {
  2595. this._order();
  2596. this._stack();
  2597. };
  2598. /**
  2599. * Order the items. The items are ordered by width first, and by left position
  2600. * second.
  2601. * If a custom order function has been provided via the options, then this will
  2602. * be used.
  2603. * @private
  2604. */
  2605. Stack.prototype._order = function _order () {
  2606. var items = this.parent.items;
  2607. if (!items) {
  2608. throw new Error('Cannot stack items: parent does not contain items');
  2609. }
  2610. // TODO: store the sorted items, to have less work later on
  2611. var ordered = [];
  2612. var index = 0;
  2613. // items is a map (no array)
  2614. util.forEach(items, function (item) {
  2615. if (item.visible) {
  2616. ordered[index] = item;
  2617. index++;
  2618. }
  2619. });
  2620. //if a customer stack order function exists, use it.
  2621. var order = this.options.order || this.defaultOptions.order;
  2622. if (!(typeof order === 'function')) {
  2623. throw new Error('Option order must be a function');
  2624. }
  2625. ordered.sort(order);
  2626. this.ordered = ordered;
  2627. };
  2628. /**
  2629. * Adjust vertical positions of the events such that they don't overlap each
  2630. * other.
  2631. * @private
  2632. */
  2633. Stack.prototype._stack = function _stack () {
  2634. var i,
  2635. iMax,
  2636. ordered = this.ordered,
  2637. options = this.options,
  2638. orientation = options.orientation || this.defaultOptions.orientation,
  2639. axisOnTop = (orientation == 'top'),
  2640. margin;
  2641. if (options.margin && options.margin.item !== undefined) {
  2642. margin = options.margin.item;
  2643. }
  2644. else {
  2645. margin = this.defaultOptions.margin.item
  2646. }
  2647. // calculate new, non-overlapping positions
  2648. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2649. var item = ordered[i];
  2650. var collidingItem = null;
  2651. do {
  2652. // TODO: optimize checking for overlap. when there is a gap without items,
  2653. // you only need to check for items from the next item on, not from zero
  2654. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2655. if (collidingItem != null) {
  2656. // There is a collision. Reposition the event above the colliding element
  2657. if (axisOnTop) {
  2658. item.top = collidingItem.top + collidingItem.height + margin;
  2659. }
  2660. else {
  2661. item.top = collidingItem.top - item.height - margin;
  2662. }
  2663. }
  2664. } while (collidingItem);
  2665. }
  2666. };
  2667. /**
  2668. * Check if the destiny position of given item overlaps with any
  2669. * of the other items from index itemStart to itemEnd.
  2670. * @param {Array} items Array with items
  2671. * @param {int} itemIndex Number of the item to be checked for overlap
  2672. * @param {int} itemStart First item to be checked.
  2673. * @param {int} itemEnd Last item to be checked.
  2674. * @return {Object | null} colliding item, or undefined when no collisions
  2675. * @param {Number} margin A minimum required margin.
  2676. * If margin is provided, the two items will be
  2677. * marked colliding when they overlap or
  2678. * when the margin between the two is smaller than
  2679. * the requested margin.
  2680. */
  2681. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2682. itemStart, itemEnd, margin) {
  2683. var collision = this.collision;
  2684. // we loop from end to start, as we suppose that the chance of a
  2685. // collision is larger for items at the end, so check these first.
  2686. var a = items[itemIndex];
  2687. for (var i = itemEnd; i >= itemStart; i--) {
  2688. var b = items[i];
  2689. if (collision(a, b, margin)) {
  2690. if (i != itemIndex) {
  2691. return b;
  2692. }
  2693. }
  2694. }
  2695. return null;
  2696. };
  2697. /**
  2698. * Test if the two provided items collide
  2699. * The items must have parameters left, width, top, and height.
  2700. * @param {Component} a The first item
  2701. * @param {Component} b The second item
  2702. * @param {Number} margin A minimum required margin.
  2703. * If margin is provided, the two items will be
  2704. * marked colliding when they overlap or
  2705. * when the margin between the two is smaller than
  2706. * the requested margin.
  2707. * @return {boolean} true if a and b collide, else false
  2708. */
  2709. Stack.prototype.collision = function collision (a, b, margin) {
  2710. return ((a.left - margin) < (b.left + b.getWidth()) &&
  2711. (a.left + a.getWidth() + margin) > b.left &&
  2712. (a.top - margin) < (b.top + b.height) &&
  2713. (a.top + a.height + margin) > b.top);
  2714. };
  2715. /**
  2716. * @constructor Range
  2717. * A Range controls a numeric range with a start and end value.
  2718. * The Range adjusts the range based on mouse events or programmatic changes,
  2719. * and triggers events when the range is changing or has been changed.
  2720. * @param {Object} [options] See description at Range.setOptions
  2721. * @extends Controller
  2722. */
  2723. function Range(options) {
  2724. this.id = util.randomUUID();
  2725. this.start = null; // Number
  2726. this.end = null; // Number
  2727. this.options = options || {};
  2728. this.setOptions(options);
  2729. }
  2730. /**
  2731. * Set options for the range controller
  2732. * @param {Object} options Available options:
  2733. * {Number} min Minimum value for start
  2734. * {Number} max Maximum value for end
  2735. * {Number} zoomMin Set a minimum value for
  2736. * (end - start).
  2737. * {Number} zoomMax Set a maximum value for
  2738. * (end - start).
  2739. */
  2740. Range.prototype.setOptions = function (options) {
  2741. util.extend(this.options, options);
  2742. // re-apply range with new limitations
  2743. if (this.start !== null && this.end !== null) {
  2744. this.setRange(this.start, this.end);
  2745. }
  2746. };
  2747. /**
  2748. * Test whether direction has a valid value
  2749. * @param {String} direction 'horizontal' or 'vertical'
  2750. */
  2751. function validateDirection (direction) {
  2752. if (direction != 'horizontal' && direction != 'vertical') {
  2753. throw new TypeError('Unknown direction "' + direction + '". ' +
  2754. 'Choose "horizontal" or "vertical".');
  2755. }
  2756. }
  2757. /**
  2758. * Add listeners for mouse and touch events to the component
  2759. * @param {Component} component
  2760. * @param {String} event Available events: 'move', 'zoom'
  2761. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2762. */
  2763. Range.prototype.subscribe = function (component, event, direction) {
  2764. var me = this;
  2765. if (event == 'move') {
  2766. // drag start listener
  2767. component.on('dragstart', function (event) {
  2768. me._onDragStart(event, component);
  2769. });
  2770. // drag listener
  2771. component.on('drag', function (event) {
  2772. me._onDrag(event, component, direction);
  2773. });
  2774. // drag end listener
  2775. component.on('dragend', function (event) {
  2776. me._onDragEnd(event, component);
  2777. });
  2778. }
  2779. else if (event == 'zoom') {
  2780. // mouse wheel
  2781. function mousewheel (event) {
  2782. me._onMouseWheel(event, component, direction);
  2783. }
  2784. component.on('mousewheel', mousewheel);
  2785. component.on('DOMMouseScroll', mousewheel); // For FF
  2786. // pinch
  2787. component.on('touch', function (event) {
  2788. me._onTouch();
  2789. });
  2790. component.on('pinch', function (event) {
  2791. me._onPinch(event, component, direction);
  2792. });
  2793. }
  2794. else {
  2795. throw new TypeError('Unknown event "' + event + '". ' +
  2796. 'Choose "move" or "zoom".');
  2797. }
  2798. };
  2799. /**
  2800. * Event handler
  2801. * @param {String} event name of the event, for example 'click', 'mousemove'
  2802. * @param {function} callback callback handler, invoked with the raw HTML Event
  2803. * as parameter.
  2804. */
  2805. Range.prototype.on = function (event, callback) {
  2806. events.addListener(this, event, callback);
  2807. };
  2808. /**
  2809. * Trigger an event
  2810. * @param {String} event name of the event, available events: 'rangechange',
  2811. * 'rangechanged'
  2812. * @private
  2813. */
  2814. Range.prototype._trigger = function (event) {
  2815. events.trigger(this, event, {
  2816. start: this.start,
  2817. end: this.end
  2818. });
  2819. };
  2820. /**
  2821. * Set a new start and end range
  2822. * @param {Number} [start]
  2823. * @param {Number} [end]
  2824. */
  2825. Range.prototype.setRange = function(start, end) {
  2826. var changed = this._applyRange(start, end);
  2827. if (changed) {
  2828. this._trigger('rangechange');
  2829. this._trigger('rangechanged');
  2830. }
  2831. };
  2832. /**
  2833. * Set a new start and end range. This method is the same as setRange, but
  2834. * does not trigger a range change and range changed event, and it returns
  2835. * true when the range is changed
  2836. * @param {Number} [start]
  2837. * @param {Number} [end]
  2838. * @return {Boolean} changed
  2839. * @private
  2840. */
  2841. Range.prototype._applyRange = function(start, end) {
  2842. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  2843. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  2844. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2845. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2846. diff;
  2847. // check for valid number
  2848. if (isNaN(newStart) || newStart === null) {
  2849. throw new Error('Invalid start "' + start + '"');
  2850. }
  2851. if (isNaN(newEnd) || newEnd === null) {
  2852. throw new Error('Invalid end "' + end + '"');
  2853. }
  2854. // prevent start < end
  2855. if (newEnd < newStart) {
  2856. newEnd = newStart;
  2857. }
  2858. // prevent start < min
  2859. if (min !== null) {
  2860. if (newStart < min) {
  2861. diff = (min - newStart);
  2862. newStart += diff;
  2863. newEnd += diff;
  2864. // prevent end > max
  2865. if (max != null) {
  2866. if (newEnd > max) {
  2867. newEnd = max;
  2868. }
  2869. }
  2870. }
  2871. }
  2872. // prevent end > max
  2873. if (max !== null) {
  2874. if (newEnd > max) {
  2875. diff = (newEnd - max);
  2876. newStart -= diff;
  2877. newEnd -= diff;
  2878. // prevent start < min
  2879. if (min != null) {
  2880. if (newStart < min) {
  2881. newStart = min;
  2882. }
  2883. }
  2884. }
  2885. }
  2886. // prevent (end-start) < zoomMin
  2887. if (this.options.zoomMin !== null) {
  2888. var zoomMin = parseFloat(this.options.zoomMin);
  2889. if (zoomMin < 0) {
  2890. zoomMin = 0;
  2891. }
  2892. if ((newEnd - newStart) < zoomMin) {
  2893. if ((this.end - this.start) === zoomMin) {
  2894. // ignore this action, we are already zoomed to the minimum
  2895. newStart = this.start;
  2896. newEnd = this.end;
  2897. }
  2898. else {
  2899. // zoom to the minimum
  2900. diff = (zoomMin - (newEnd - newStart));
  2901. newStart -= diff / 2;
  2902. newEnd += diff / 2;
  2903. }
  2904. }
  2905. }
  2906. // prevent (end-start) > zoomMax
  2907. if (this.options.zoomMax !== null) {
  2908. var zoomMax = parseFloat(this.options.zoomMax);
  2909. if (zoomMax < 0) {
  2910. zoomMax = 0;
  2911. }
  2912. if ((newEnd - newStart) > zoomMax) {
  2913. if ((this.end - this.start) === zoomMax) {
  2914. // ignore this action, we are already zoomed to the maximum
  2915. newStart = this.start;
  2916. newEnd = this.end;
  2917. }
  2918. else {
  2919. // zoom to the maximum
  2920. diff = ((newEnd - newStart) - zoomMax);
  2921. newStart += diff / 2;
  2922. newEnd -= diff / 2;
  2923. }
  2924. }
  2925. }
  2926. var changed = (this.start != newStart || this.end != newEnd);
  2927. this.start = newStart;
  2928. this.end = newEnd;
  2929. return changed;
  2930. };
  2931. /**
  2932. * Retrieve the current range.
  2933. * @return {Object} An object with start and end properties
  2934. */
  2935. Range.prototype.getRange = function() {
  2936. return {
  2937. start: this.start,
  2938. end: this.end
  2939. };
  2940. };
  2941. /**
  2942. * Calculate the conversion offset and scale for current range, based on
  2943. * the provided width
  2944. * @param {Number} width
  2945. * @returns {{offset: number, scale: number}} conversion
  2946. */
  2947. Range.prototype.conversion = function (width) {
  2948. return Range.conversion(this.start, this.end, width);
  2949. };
  2950. /**
  2951. * Static method to calculate the conversion offset and scale for a range,
  2952. * based on the provided start, end, and width
  2953. * @param {Number} start
  2954. * @param {Number} end
  2955. * @param {Number} width
  2956. * @returns {{offset: number, scale: number}} conversion
  2957. */
  2958. Range.conversion = function (start, end, width) {
  2959. if (width != 0 && (end - start != 0)) {
  2960. return {
  2961. offset: start,
  2962. scale: width / (end - start)
  2963. }
  2964. }
  2965. else {
  2966. return {
  2967. offset: 0,
  2968. scale: 1
  2969. };
  2970. }
  2971. };
  2972. // global (private) object to store drag params
  2973. var touchParams = {};
  2974. /**
  2975. * Start dragging horizontally or vertically
  2976. * @param {Event} event
  2977. * @param {Object} component
  2978. * @private
  2979. */
  2980. Range.prototype._onDragStart = function(event, component) {
  2981. // refuse to drag when we where pinching to prevent the timeline make a jump
  2982. // when releasing the fingers in opposite order from the touch screen
  2983. if (touchParams.pinching) return;
  2984. touchParams.start = this.start;
  2985. touchParams.end = this.end;
  2986. var frame = component.frame;
  2987. if (frame) {
  2988. frame.style.cursor = 'move';
  2989. }
  2990. };
  2991. /**
  2992. * Perform dragging operating.
  2993. * @param {Event} event
  2994. * @param {Component} component
  2995. * @param {String} direction 'horizontal' or 'vertical'
  2996. * @private
  2997. */
  2998. Range.prototype._onDrag = function (event, component, direction) {
  2999. validateDirection(direction);
  3000. // refuse to drag when we where pinching to prevent the timeline make a jump
  3001. // when releasing the fingers in opposite order from the touch screen
  3002. if (touchParams.pinching) return;
  3003. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3004. interval = (touchParams.end - touchParams.start),
  3005. width = (direction == 'horizontal') ? component.width : component.height,
  3006. diffRange = -delta / width * interval;
  3007. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3008. // fire a rangechange event
  3009. this._trigger('rangechange');
  3010. };
  3011. /**
  3012. * Stop dragging operating.
  3013. * @param {event} event
  3014. * @param {Component} component
  3015. * @private
  3016. */
  3017. Range.prototype._onDragEnd = function (event, component) {
  3018. // refuse to drag when we where pinching to prevent the timeline make a jump
  3019. // when releasing the fingers in opposite order from the touch screen
  3020. if (touchParams.pinching) return;
  3021. if (component.frame) {
  3022. component.frame.style.cursor = 'auto';
  3023. }
  3024. // fire a rangechanged event
  3025. this._trigger('rangechanged');
  3026. };
  3027. /**
  3028. * Event handler for mouse wheel event, used to zoom
  3029. * Code from http://adomas.org/javascript-mouse-wheel/
  3030. * @param {Event} event
  3031. * @param {Component} component
  3032. * @param {String} direction 'horizontal' or 'vertical'
  3033. * @private
  3034. */
  3035. Range.prototype._onMouseWheel = function(event, component, direction) {
  3036. validateDirection(direction);
  3037. // retrieve delta
  3038. var delta = 0;
  3039. if (event.wheelDelta) { /* IE/Opera. */
  3040. delta = event.wheelDelta / 120;
  3041. } else if (event.detail) { /* Mozilla case. */
  3042. // In Mozilla, sign of delta is different than in IE.
  3043. // Also, delta is multiple of 3.
  3044. delta = -event.detail / 3;
  3045. }
  3046. // If delta is nonzero, handle it.
  3047. // Basically, delta is now positive if wheel was scrolled up,
  3048. // and negative, if wheel was scrolled down.
  3049. if (delta) {
  3050. // perform the zoom action. Delta is normally 1 or -1
  3051. // adjust a negative delta such that zooming in with delta 0.1
  3052. // equals zooming out with a delta -0.1
  3053. var scale;
  3054. if (delta < 0) {
  3055. scale = 1 - (delta / 5);
  3056. }
  3057. else {
  3058. scale = 1 / (1 + (delta / 5)) ;
  3059. }
  3060. // calculate center, the date to zoom around
  3061. var gesture = util.fakeGesture(this, event),
  3062. pointer = getPointer(gesture.touches[0], component.frame),
  3063. pointerDate = this._pointerToDate(component, direction, pointer);
  3064. this.zoom(scale, pointerDate);
  3065. }
  3066. // Prevent default actions caused by mouse wheel
  3067. // (else the page and timeline both zoom and scroll)
  3068. util.preventDefault(event);
  3069. };
  3070. /**
  3071. * On start of a touch gesture, initialize scale to 1
  3072. * @private
  3073. */
  3074. Range.prototype._onTouch = function () {
  3075. touchParams.start = this.start;
  3076. touchParams.end = this.end;
  3077. touchParams.pinching = false;
  3078. touchParams.center = null;
  3079. };
  3080. /**
  3081. * Handle pinch event
  3082. * @param {Event} event
  3083. * @param {Component} component
  3084. * @param {String} direction 'horizontal' or 'vertical'
  3085. * @private
  3086. */
  3087. Range.prototype._onPinch = function (event, component, direction) {
  3088. touchParams.pinching = true;
  3089. if (event.gesture.touches.length > 1) {
  3090. if (!touchParams.center) {
  3091. touchParams.center = getPointer(event.gesture.center, component.frame);
  3092. }
  3093. var scale = 1 / event.gesture.scale,
  3094. initDate = this._pointerToDate(component, direction, touchParams.center),
  3095. center = getPointer(event.gesture.center, component.frame),
  3096. date = this._pointerToDate(component, direction, center),
  3097. delta = date - initDate; // TODO: utilize delta
  3098. // calculate new start and end
  3099. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3100. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3101. // apply new range
  3102. this.setRange(newStart, newEnd);
  3103. }
  3104. };
  3105. /**
  3106. * Helper function to calculate the center date for zooming
  3107. * @param {Component} component
  3108. * @param {{x: Number, y: Number}} pointer
  3109. * @param {String} direction 'horizontal' or 'vertical'
  3110. * @return {number} date
  3111. * @private
  3112. */
  3113. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3114. var conversion;
  3115. if (direction == 'horizontal') {
  3116. var width = component.width;
  3117. conversion = this.conversion(width);
  3118. return pointer.x / conversion.scale + conversion.offset;
  3119. }
  3120. else {
  3121. var height = component.height;
  3122. conversion = this.conversion(height);
  3123. return pointer.y / conversion.scale + conversion.offset;
  3124. }
  3125. };
  3126. /**
  3127. * Get the pointer location relative to the location of the dom element
  3128. * @param {{pageX: Number, pageY: Number}} touch
  3129. * @param {Element} element HTML DOM element
  3130. * @return {{x: Number, y: Number}} pointer
  3131. * @private
  3132. */
  3133. function getPointer (touch, element) {
  3134. return {
  3135. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3136. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3137. };
  3138. }
  3139. /**
  3140. * Zoom the range the given scale in or out. Start and end date will
  3141. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3142. * date around which to zoom.
  3143. * For example, try scale = 0.9 or 1.1
  3144. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3145. * values below 1 will zoom in.
  3146. * @param {Number} [center] Value representing a date around which will
  3147. * be zoomed.
  3148. */
  3149. Range.prototype.zoom = function(scale, center) {
  3150. // if centerDate is not provided, take it half between start Date and end Date
  3151. if (center == null) {
  3152. center = (this.start + this.end) / 2;
  3153. }
  3154. // calculate new start and end
  3155. var newStart = center + (this.start - center) * scale;
  3156. var newEnd = center + (this.end - center) * scale;
  3157. this.setRange(newStart, newEnd);
  3158. };
  3159. /**
  3160. * Move the range with a given delta to the left or right. Start and end
  3161. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3162. * @param {Number} delta Moving amount. Positive value will move right,
  3163. * negative value will move left
  3164. */
  3165. Range.prototype.move = function(delta) {
  3166. // zoom start Date and end Date relative to the centerDate
  3167. var diff = (this.end - this.start);
  3168. // apply new values
  3169. var newStart = this.start + diff * delta;
  3170. var newEnd = this.end + diff * delta;
  3171. // TODO: reckon with min and max range
  3172. this.start = newStart;
  3173. this.end = newEnd;
  3174. };
  3175. /**
  3176. * Move the range to a new center point
  3177. * @param {Number} moveTo New center point of the range
  3178. */
  3179. Range.prototype.moveTo = function(moveTo) {
  3180. var center = (this.start + this.end) / 2;
  3181. var diff = center - moveTo;
  3182. // calculate new start and end
  3183. var newStart = this.start - diff;
  3184. var newEnd = this.end - diff;
  3185. this.setRange(newStart, newEnd);
  3186. };
  3187. /**
  3188. * @constructor Controller
  3189. *
  3190. * A Controller controls the reflows and repaints of all visual components
  3191. */
  3192. function Controller () {
  3193. this.id = util.randomUUID();
  3194. this.components = {};
  3195. this.repaintTimer = undefined;
  3196. this.reflowTimer = undefined;
  3197. }
  3198. /**
  3199. * Add a component to the controller
  3200. * @param {Component} component
  3201. */
  3202. Controller.prototype.add = function add(component) {
  3203. // validate the component
  3204. if (component.id == undefined) {
  3205. throw new Error('Component has no field id');
  3206. }
  3207. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3208. throw new TypeError('Component must be an instance of ' +
  3209. 'prototype Component or Controller');
  3210. }
  3211. // add the component
  3212. component.controller = this;
  3213. this.components[component.id] = component;
  3214. };
  3215. /**
  3216. * Remove a component from the controller
  3217. * @param {Component | String} component
  3218. */
  3219. Controller.prototype.remove = function remove(component) {
  3220. var id;
  3221. for (id in this.components) {
  3222. if (this.components.hasOwnProperty(id)) {
  3223. if (id == component || this.components[id] == component) {
  3224. break;
  3225. }
  3226. }
  3227. }
  3228. if (id) {
  3229. delete this.components[id];
  3230. }
  3231. };
  3232. /**
  3233. * Request a reflow. The controller will schedule a reflow
  3234. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3235. * is false.
  3236. */
  3237. Controller.prototype.requestReflow = function requestReflow(force) {
  3238. if (force) {
  3239. this.reflow();
  3240. }
  3241. else {
  3242. if (!this.reflowTimer) {
  3243. var me = this;
  3244. this.reflowTimer = setTimeout(function () {
  3245. me.reflowTimer = undefined;
  3246. me.reflow();
  3247. }, 0);
  3248. }
  3249. }
  3250. };
  3251. /**
  3252. * Request a repaint. The controller will schedule a repaint
  3253. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3254. * is false.
  3255. */
  3256. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3257. if (force) {
  3258. this.repaint();
  3259. }
  3260. else {
  3261. if (!this.repaintTimer) {
  3262. var me = this;
  3263. this.repaintTimer = setTimeout(function () {
  3264. me.repaintTimer = undefined;
  3265. me.repaint();
  3266. }, 0);
  3267. }
  3268. }
  3269. };
  3270. /**
  3271. * Repaint all components
  3272. */
  3273. Controller.prototype.repaint = function repaint() {
  3274. var changed = false;
  3275. // cancel any running repaint request
  3276. if (this.repaintTimer) {
  3277. clearTimeout(this.repaintTimer);
  3278. this.repaintTimer = undefined;
  3279. }
  3280. var done = {};
  3281. function repaint(component, id) {
  3282. if (!(id in done)) {
  3283. // first repaint the components on which this component is dependent
  3284. if (component.depends) {
  3285. component.depends.forEach(function (dep) {
  3286. repaint(dep, dep.id);
  3287. });
  3288. }
  3289. if (component.parent) {
  3290. repaint(component.parent, component.parent.id);
  3291. }
  3292. // repaint the component itself and mark as done
  3293. changed = component.repaint() || changed;
  3294. done[id] = true;
  3295. }
  3296. }
  3297. util.forEach(this.components, repaint);
  3298. // immediately reflow when needed
  3299. if (changed) {
  3300. this.reflow();
  3301. }
  3302. // TODO: limit the number of nested reflows/repaints, prevent loop
  3303. };
  3304. /**
  3305. * Reflow all components
  3306. */
  3307. Controller.prototype.reflow = function reflow() {
  3308. var resized = false;
  3309. // cancel any running repaint request
  3310. if (this.reflowTimer) {
  3311. clearTimeout(this.reflowTimer);
  3312. this.reflowTimer = undefined;
  3313. }
  3314. var done = {};
  3315. function reflow(component, id) {
  3316. if (!(id in done)) {
  3317. // first reflow the components on which this component is dependent
  3318. if (component.depends) {
  3319. component.depends.forEach(function (dep) {
  3320. reflow(dep, dep.id);
  3321. });
  3322. }
  3323. if (component.parent) {
  3324. reflow(component.parent, component.parent.id);
  3325. }
  3326. // reflow the component itself and mark as done
  3327. resized = component.reflow() || resized;
  3328. done[id] = true;
  3329. }
  3330. }
  3331. util.forEach(this.components, reflow);
  3332. // immediately repaint when needed
  3333. if (resized) {
  3334. this.repaint();
  3335. }
  3336. // TODO: limit the number of nested reflows/repaints, prevent loop
  3337. };
  3338. /**
  3339. * Prototype for visual components
  3340. */
  3341. function Component () {
  3342. this.id = null;
  3343. this.parent = null;
  3344. this.depends = null;
  3345. this.controller = null;
  3346. this.options = null;
  3347. this.frame = null; // main DOM element
  3348. this.top = 0;
  3349. this.left = 0;
  3350. this.width = 0;
  3351. this.height = 0;
  3352. }
  3353. /**
  3354. * Set parameters for the frame. Parameters will be merged in current parameter
  3355. * set.
  3356. * @param {Object} options Available parameters:
  3357. * {String | function} [className]
  3358. * {EventBus} [eventBus]
  3359. * {String | Number | function} [left]
  3360. * {String | Number | function} [top]
  3361. * {String | Number | function} [width]
  3362. * {String | Number | function} [height]
  3363. */
  3364. Component.prototype.setOptions = function setOptions(options) {
  3365. if (options) {
  3366. util.extend(this.options, options);
  3367. if (this.controller) {
  3368. this.requestRepaint();
  3369. this.requestReflow();
  3370. }
  3371. }
  3372. };
  3373. /**
  3374. * Get an option value by name
  3375. * The function will first check this.options object, and else will check
  3376. * this.defaultOptions.
  3377. * @param {String} name
  3378. * @return {*} value
  3379. */
  3380. Component.prototype.getOption = function getOption(name) {
  3381. var value;
  3382. if (this.options) {
  3383. value = this.options[name];
  3384. }
  3385. if (value === undefined && this.defaultOptions) {
  3386. value = this.defaultOptions[name];
  3387. }
  3388. return value;
  3389. };
  3390. /**
  3391. * Get the container element of the component, which can be used by a child to
  3392. * add its own widgets. Not all components do have a container for childs, in
  3393. * that case null is returned.
  3394. * @returns {HTMLElement | null} container
  3395. */
  3396. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3397. Component.prototype.getContainer = function getContainer() {
  3398. // should be implemented by the component
  3399. return null;
  3400. };
  3401. /**
  3402. * Get the frame element of the component, the outer HTML DOM element.
  3403. * @returns {HTMLElement | null} frame
  3404. */
  3405. Component.prototype.getFrame = function getFrame() {
  3406. return this.frame;
  3407. };
  3408. /**
  3409. * Repaint the component
  3410. * @return {Boolean} changed
  3411. */
  3412. Component.prototype.repaint = function repaint() {
  3413. // should be implemented by the component
  3414. return false;
  3415. };
  3416. /**
  3417. * Reflow the component
  3418. * @return {Boolean} resized
  3419. */
  3420. Component.prototype.reflow = function reflow() {
  3421. // should be implemented by the component
  3422. return false;
  3423. };
  3424. /**
  3425. * Hide the component from the DOM
  3426. * @return {Boolean} changed
  3427. */
  3428. Component.prototype.hide = function hide() {
  3429. if (this.frame && this.frame.parentNode) {
  3430. this.frame.parentNode.removeChild(this.frame);
  3431. return true;
  3432. }
  3433. else {
  3434. return false;
  3435. }
  3436. };
  3437. /**
  3438. * Show the component in the DOM (when not already visible).
  3439. * A repaint will be executed when the component is not visible
  3440. * @return {Boolean} changed
  3441. */
  3442. Component.prototype.show = function show() {
  3443. if (!this.frame || !this.frame.parentNode) {
  3444. return this.repaint();
  3445. }
  3446. else {
  3447. return false;
  3448. }
  3449. };
  3450. /**
  3451. * Request a repaint. The controller will schedule a repaint
  3452. */
  3453. Component.prototype.requestRepaint = function requestRepaint() {
  3454. if (this.controller) {
  3455. this.controller.requestRepaint();
  3456. }
  3457. else {
  3458. throw new Error('Cannot request a repaint: no controller configured');
  3459. // TODO: just do a repaint when no parent is configured?
  3460. }
  3461. };
  3462. /**
  3463. * Request a reflow. The controller will schedule a reflow
  3464. */
  3465. Component.prototype.requestReflow = function requestReflow() {
  3466. if (this.controller) {
  3467. this.controller.requestReflow();
  3468. }
  3469. else {
  3470. throw new Error('Cannot request a reflow: no controller configured');
  3471. // TODO: just do a reflow when no parent is configured?
  3472. }
  3473. };
  3474. /**
  3475. * A panel can contain components
  3476. * @param {Component} [parent]
  3477. * @param {Component[]} [depends] Components on which this components depends
  3478. * (except for the parent)
  3479. * @param {Object} [options] Available parameters:
  3480. * {String | Number | function} [left]
  3481. * {String | Number | function} [top]
  3482. * {String | Number | function} [width]
  3483. * {String | Number | function} [height]
  3484. * {String | function} [className]
  3485. * @constructor Panel
  3486. * @extends Component
  3487. */
  3488. function Panel(parent, depends, options) {
  3489. this.id = util.randomUUID();
  3490. this.parent = parent;
  3491. this.depends = depends;
  3492. this.options = options || {};
  3493. }
  3494. Panel.prototype = new Component();
  3495. /**
  3496. * Set options. Will extend the current options.
  3497. * @param {Object} [options] Available parameters:
  3498. * {String | function} [className]
  3499. * {String | Number | function} [left]
  3500. * {String | Number | function} [top]
  3501. * {String | Number | function} [width]
  3502. * {String | Number | function} [height]
  3503. */
  3504. Panel.prototype.setOptions = Component.prototype.setOptions;
  3505. /**
  3506. * Get the container element of the panel, which can be used by a child to
  3507. * add its own widgets.
  3508. * @returns {HTMLElement} container
  3509. */
  3510. Panel.prototype.getContainer = function () {
  3511. return this.frame;
  3512. };
  3513. /**
  3514. * Repaint the component
  3515. * @return {Boolean} changed
  3516. */
  3517. Panel.prototype.repaint = function () {
  3518. var changed = 0,
  3519. update = util.updateProperty,
  3520. asSize = util.option.asSize,
  3521. options = this.options,
  3522. frame = this.frame;
  3523. if (!frame) {
  3524. frame = document.createElement('div');
  3525. frame.className = 'panel';
  3526. var className = options.className;
  3527. if (className) {
  3528. if (typeof className == 'function') {
  3529. util.addClassName(frame, String(className()));
  3530. }
  3531. else {
  3532. util.addClassName(frame, String(className));
  3533. }
  3534. }
  3535. this.frame = frame;
  3536. changed += 1;
  3537. }
  3538. if (!frame.parentNode) {
  3539. if (!this.parent) {
  3540. throw new Error('Cannot repaint panel: no parent attached');
  3541. }
  3542. var parentContainer = this.parent.getContainer();
  3543. if (!parentContainer) {
  3544. throw new Error('Cannot repaint panel: parent has no container element');
  3545. }
  3546. parentContainer.appendChild(frame);
  3547. changed += 1;
  3548. }
  3549. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3550. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3551. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3552. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3553. return (changed > 0);
  3554. };
  3555. /**
  3556. * Reflow the component
  3557. * @return {Boolean} resized
  3558. */
  3559. Panel.prototype.reflow = function () {
  3560. var changed = 0,
  3561. update = util.updateProperty,
  3562. frame = this.frame;
  3563. if (frame) {
  3564. changed += update(this, 'top', frame.offsetTop);
  3565. changed += update(this, 'left', frame.offsetLeft);
  3566. changed += update(this, 'width', frame.offsetWidth);
  3567. changed += update(this, 'height', frame.offsetHeight);
  3568. }
  3569. else {
  3570. changed += 1;
  3571. }
  3572. return (changed > 0);
  3573. };
  3574. /**
  3575. * A root panel can hold components. The root panel must be initialized with
  3576. * a DOM element as container.
  3577. * @param {HTMLElement} container
  3578. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3579. * @constructor RootPanel
  3580. * @extends Panel
  3581. */
  3582. function RootPanel(container, options) {
  3583. this.id = util.randomUUID();
  3584. this.container = container;
  3585. this.options = options || {};
  3586. this.defaultOptions = {
  3587. autoResize: true
  3588. };
  3589. this.listeners = {}; // event listeners
  3590. }
  3591. RootPanel.prototype = new Panel();
  3592. /**
  3593. * Set options. Will extend the current options.
  3594. * @param {Object} [options] Available parameters:
  3595. * {String | function} [className]
  3596. * {String | Number | function} [left]
  3597. * {String | Number | function} [top]
  3598. * {String | Number | function} [width]
  3599. * {String | Number | function} [height]
  3600. * {Boolean | function} [autoResize]
  3601. */
  3602. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3603. /**
  3604. * Repaint the component
  3605. * @return {Boolean} changed
  3606. */
  3607. RootPanel.prototype.repaint = function () {
  3608. var changed = 0,
  3609. update = util.updateProperty,
  3610. asSize = util.option.asSize,
  3611. options = this.options,
  3612. frame = this.frame;
  3613. if (!frame) {
  3614. frame = document.createElement('div');
  3615. this.frame = frame;
  3616. changed += 1;
  3617. }
  3618. if (!frame.parentNode) {
  3619. if (!this.container) {
  3620. throw new Error('Cannot repaint root panel: no container attached');
  3621. }
  3622. this.container.appendChild(frame);
  3623. changed += 1;
  3624. }
  3625. frame.className = 'vis timeline rootpanel ' + options.orientation;
  3626. var className = options.className;
  3627. if (className) {
  3628. util.addClassName(frame, util.option.asString(className));
  3629. }
  3630. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3631. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3632. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3633. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3634. this._updateEventEmitters();
  3635. this._updateWatch();
  3636. return (changed > 0);
  3637. };
  3638. /**
  3639. * Reflow the component
  3640. * @return {Boolean} resized
  3641. */
  3642. RootPanel.prototype.reflow = function () {
  3643. var changed = 0,
  3644. update = util.updateProperty,
  3645. frame = this.frame;
  3646. if (frame) {
  3647. changed += update(this, 'top', frame.offsetTop);
  3648. changed += update(this, 'left', frame.offsetLeft);
  3649. changed += update(this, 'width', frame.offsetWidth);
  3650. changed += update(this, 'height', frame.offsetHeight);
  3651. }
  3652. else {
  3653. changed += 1;
  3654. }
  3655. return (changed > 0);
  3656. };
  3657. /**
  3658. * Update watching for resize, depending on the current option
  3659. * @private
  3660. */
  3661. RootPanel.prototype._updateWatch = function () {
  3662. var autoResize = this.getOption('autoResize');
  3663. if (autoResize) {
  3664. this._watch();
  3665. }
  3666. else {
  3667. this._unwatch();
  3668. }
  3669. };
  3670. /**
  3671. * Watch for changes in the size of the frame. On resize, the Panel will
  3672. * automatically redraw itself.
  3673. * @private
  3674. */
  3675. RootPanel.prototype._watch = function () {
  3676. var me = this;
  3677. this._unwatch();
  3678. var checkSize = function () {
  3679. var autoResize = me.getOption('autoResize');
  3680. if (!autoResize) {
  3681. // stop watching when the option autoResize is changed to false
  3682. me._unwatch();
  3683. return;
  3684. }
  3685. if (me.frame) {
  3686. // check whether the frame is resized
  3687. if ((me.frame.clientWidth != me.width) ||
  3688. (me.frame.clientHeight != me.height)) {
  3689. me.requestReflow();
  3690. }
  3691. }
  3692. };
  3693. // TODO: automatically cleanup the event listener when the frame is deleted
  3694. util.addEventListener(window, 'resize', checkSize);
  3695. this.watchTimer = setInterval(checkSize, 1000);
  3696. };
  3697. /**
  3698. * Stop watching for a resize of the frame.
  3699. * @private
  3700. */
  3701. RootPanel.prototype._unwatch = function () {
  3702. if (this.watchTimer) {
  3703. clearInterval(this.watchTimer);
  3704. this.watchTimer = undefined;
  3705. }
  3706. // TODO: remove event listener on window.resize
  3707. };
  3708. /**
  3709. * Event handler
  3710. * @param {String} event name of the event, for example 'click', 'mousemove'
  3711. * @param {function} callback callback handler, invoked with the raw HTML Event
  3712. * as parameter.
  3713. */
  3714. RootPanel.prototype.on = function (event, callback) {
  3715. // register the listener at this component
  3716. var arr = this.listeners[event];
  3717. if (!arr) {
  3718. arr = [];
  3719. this.listeners[event] = arr;
  3720. }
  3721. arr.push(callback);
  3722. this._updateEventEmitters();
  3723. };
  3724. /**
  3725. * Update the event listeners for all event emitters
  3726. * @private
  3727. */
  3728. RootPanel.prototype._updateEventEmitters = function () {
  3729. if (this.listeners) {
  3730. var me = this;
  3731. util.forEach(this.listeners, function (listeners, event) {
  3732. if (!me.emitters) {
  3733. me.emitters = {};
  3734. }
  3735. if (!(event in me.emitters)) {
  3736. // create event
  3737. var frame = me.frame;
  3738. if (frame) {
  3739. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3740. var callback = function(event) {
  3741. listeners.forEach(function (listener) {
  3742. // TODO: filter on event target!
  3743. listener(event);
  3744. });
  3745. };
  3746. me.emitters[event] = callback;
  3747. if (!me.hammer) {
  3748. me.hammer = Hammer(frame, {
  3749. prevent_default: true
  3750. });
  3751. }
  3752. me.hammer.on(event, callback);
  3753. }
  3754. }
  3755. });
  3756. // TODO: be able to delete event listeners
  3757. // TODO: be able to move event listeners to a parent when available
  3758. }
  3759. };
  3760. /**
  3761. * A horizontal time axis
  3762. * @param {Component} parent
  3763. * @param {Component[]} [depends] Components on which this components depends
  3764. * (except for the parent)
  3765. * @param {Object} [options] See TimeAxis.setOptions for the available
  3766. * options.
  3767. * @constructor TimeAxis
  3768. * @extends Component
  3769. */
  3770. function TimeAxis (parent, depends, options) {
  3771. this.id = util.randomUUID();
  3772. this.parent = parent;
  3773. this.depends = depends;
  3774. this.dom = {
  3775. majorLines: [],
  3776. majorTexts: [],
  3777. minorLines: [],
  3778. minorTexts: [],
  3779. redundant: {
  3780. majorLines: [],
  3781. majorTexts: [],
  3782. minorLines: [],
  3783. minorTexts: []
  3784. }
  3785. };
  3786. this.props = {
  3787. range: {
  3788. start: 0,
  3789. end: 0,
  3790. minimumStep: 0
  3791. },
  3792. lineTop: 0
  3793. };
  3794. this.options = options || {};
  3795. this.defaultOptions = {
  3796. orientation: 'bottom', // supported: 'top', 'bottom'
  3797. // TODO: implement timeaxis orientations 'left' and 'right'
  3798. showMinorLabels: true,
  3799. showMajorLabels: true
  3800. };
  3801. this.conversion = null;
  3802. this.range = null;
  3803. }
  3804. TimeAxis.prototype = new Component();
  3805. // TODO: comment options
  3806. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3807. /**
  3808. * Set a range (start and end)
  3809. * @param {Range | Object} range A Range or an object containing start and end.
  3810. */
  3811. TimeAxis.prototype.setRange = function (range) {
  3812. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3813. throw new TypeError('Range must be an instance of Range, ' +
  3814. 'or an object containing start and end.');
  3815. }
  3816. this.range = range;
  3817. };
  3818. /**
  3819. * Convert a position on screen (pixels) to a datetime
  3820. * @param {int} x Position on the screen in pixels
  3821. * @return {Date} time The datetime the corresponds with given position x
  3822. */
  3823. TimeAxis.prototype.toTime = function(x) {
  3824. var conversion = this.conversion;
  3825. return new Date(x / conversion.scale + conversion.offset);
  3826. };
  3827. /**
  3828. * Convert a datetime (Date object) into a position on the screen
  3829. * @param {Date} time A date
  3830. * @return {int} x The position on the screen in pixels which corresponds
  3831. * with the given date.
  3832. * @private
  3833. */
  3834. TimeAxis.prototype.toScreen = function(time) {
  3835. var conversion = this.conversion;
  3836. return (time.valueOf() - conversion.offset) * conversion.scale;
  3837. };
  3838. /**
  3839. * Repaint the component
  3840. * @return {Boolean} changed
  3841. */
  3842. TimeAxis.prototype.repaint = function () {
  3843. var changed = 0,
  3844. update = util.updateProperty,
  3845. asSize = util.option.asSize,
  3846. options = this.options,
  3847. orientation = this.getOption('orientation'),
  3848. props = this.props,
  3849. step = this.step;
  3850. var frame = this.frame;
  3851. if (!frame) {
  3852. frame = document.createElement('div');
  3853. this.frame = frame;
  3854. changed += 1;
  3855. }
  3856. frame.className = 'axis';
  3857. // TODO: custom className?
  3858. if (!frame.parentNode) {
  3859. if (!this.parent) {
  3860. throw new Error('Cannot repaint time axis: no parent attached');
  3861. }
  3862. var parentContainer = this.parent.getContainer();
  3863. if (!parentContainer) {
  3864. throw new Error('Cannot repaint time axis: parent has no container element');
  3865. }
  3866. parentContainer.appendChild(frame);
  3867. changed += 1;
  3868. }
  3869. var parent = frame.parentNode;
  3870. if (parent) {
  3871. var beforeChild = frame.nextSibling;
  3872. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3873. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3874. (this.props.parentHeight - this.height) + 'px' :
  3875. '0px';
  3876. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3877. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3878. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3879. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3880. // get characters width and height
  3881. this._repaintMeasureChars();
  3882. if (this.step) {
  3883. this._repaintStart();
  3884. step.first();
  3885. var xFirstMajorLabel = undefined;
  3886. var max = 0;
  3887. while (step.hasNext() && max < 1000) {
  3888. max++;
  3889. var cur = step.getCurrent(),
  3890. x = this.toScreen(cur),
  3891. isMajor = step.isMajor();
  3892. // TODO: lines must have a width, such that we can create css backgrounds
  3893. if (this.getOption('showMinorLabels')) {
  3894. this._repaintMinorText(x, step.getLabelMinor());
  3895. }
  3896. if (isMajor && this.getOption('showMajorLabels')) {
  3897. if (x > 0) {
  3898. if (xFirstMajorLabel == undefined) {
  3899. xFirstMajorLabel = x;
  3900. }
  3901. this._repaintMajorText(x, step.getLabelMajor());
  3902. }
  3903. this._repaintMajorLine(x);
  3904. }
  3905. else {
  3906. this._repaintMinorLine(x);
  3907. }
  3908. step.next();
  3909. }
  3910. // create a major label on the left when needed
  3911. if (this.getOption('showMajorLabels')) {
  3912. var leftTime = this.toTime(0),
  3913. leftText = step.getLabelMajor(leftTime),
  3914. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3915. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3916. this._repaintMajorText(0, leftText);
  3917. }
  3918. }
  3919. this._repaintEnd();
  3920. }
  3921. this._repaintLine();
  3922. // put frame online again
  3923. if (beforeChild) {
  3924. parent.insertBefore(frame, beforeChild);
  3925. }
  3926. else {
  3927. parent.appendChild(frame)
  3928. }
  3929. }
  3930. return (changed > 0);
  3931. };
  3932. /**
  3933. * Start a repaint. Move all DOM elements to a redundant list, where they
  3934. * can be picked for re-use, or can be cleaned up in the end
  3935. * @private
  3936. */
  3937. TimeAxis.prototype._repaintStart = function () {
  3938. var dom = this.dom,
  3939. redundant = dom.redundant;
  3940. redundant.majorLines = dom.majorLines;
  3941. redundant.majorTexts = dom.majorTexts;
  3942. redundant.minorLines = dom.minorLines;
  3943. redundant.minorTexts = dom.minorTexts;
  3944. dom.majorLines = [];
  3945. dom.majorTexts = [];
  3946. dom.minorLines = [];
  3947. dom.minorTexts = [];
  3948. };
  3949. /**
  3950. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3951. * @private
  3952. */
  3953. TimeAxis.prototype._repaintEnd = function () {
  3954. util.forEach(this.dom.redundant, function (arr) {
  3955. while (arr.length) {
  3956. var elem = arr.pop();
  3957. if (elem && elem.parentNode) {
  3958. elem.parentNode.removeChild(elem);
  3959. }
  3960. }
  3961. });
  3962. };
  3963. /**
  3964. * Create a minor label for the axis at position x
  3965. * @param {Number} x
  3966. * @param {String} text
  3967. * @private
  3968. */
  3969. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3970. // reuse redundant label
  3971. var label = this.dom.redundant.minorTexts.shift();
  3972. if (!label) {
  3973. // create new label
  3974. var content = document.createTextNode('');
  3975. label = document.createElement('div');
  3976. label.appendChild(content);
  3977. label.className = 'text minor';
  3978. this.frame.appendChild(label);
  3979. }
  3980. this.dom.minorTexts.push(label);
  3981. label.childNodes[0].nodeValue = text;
  3982. label.style.left = x + 'px';
  3983. label.style.top = this.props.minorLabelTop + 'px';
  3984. //label.title = title; // TODO: this is a heavy operation
  3985. };
  3986. /**
  3987. * Create a Major label for the axis at position x
  3988. * @param {Number} x
  3989. * @param {String} text
  3990. * @private
  3991. */
  3992. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3993. // reuse redundant label
  3994. var label = this.dom.redundant.majorTexts.shift();
  3995. if (!label) {
  3996. // create label
  3997. var content = document.createTextNode(text);
  3998. label = document.createElement('div');
  3999. label.className = 'text major';
  4000. label.appendChild(content);
  4001. this.frame.appendChild(label);
  4002. }
  4003. this.dom.majorTexts.push(label);
  4004. label.childNodes[0].nodeValue = text;
  4005. label.style.top = this.props.majorLabelTop + 'px';
  4006. label.style.left = x + 'px';
  4007. //label.title = title; // TODO: this is a heavy operation
  4008. };
  4009. /**
  4010. * Create a minor line for the axis at position x
  4011. * @param {Number} x
  4012. * @private
  4013. */
  4014. TimeAxis.prototype._repaintMinorLine = function (x) {
  4015. // reuse redundant line
  4016. var line = this.dom.redundant.minorLines.shift();
  4017. if (!line) {
  4018. // create vertical line
  4019. line = document.createElement('div');
  4020. line.className = 'grid vertical minor';
  4021. this.frame.appendChild(line);
  4022. }
  4023. this.dom.minorLines.push(line);
  4024. var props = this.props;
  4025. line.style.top = props.minorLineTop + 'px';
  4026. line.style.height = props.minorLineHeight + 'px';
  4027. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4028. };
  4029. /**
  4030. * Create a Major line for the axis at position x
  4031. * @param {Number} x
  4032. * @private
  4033. */
  4034. TimeAxis.prototype._repaintMajorLine = function (x) {
  4035. // reuse redundant line
  4036. var line = this.dom.redundant.majorLines.shift();
  4037. if (!line) {
  4038. // create vertical line
  4039. line = document.createElement('DIV');
  4040. line.className = 'grid vertical major';
  4041. this.frame.appendChild(line);
  4042. }
  4043. this.dom.majorLines.push(line);
  4044. var props = this.props;
  4045. line.style.top = props.majorLineTop + 'px';
  4046. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4047. line.style.height = props.majorLineHeight + 'px';
  4048. };
  4049. /**
  4050. * Repaint the horizontal line for the axis
  4051. * @private
  4052. */
  4053. TimeAxis.prototype._repaintLine = function() {
  4054. var line = this.dom.line,
  4055. frame = this.frame,
  4056. options = this.options;
  4057. // line before all axis elements
  4058. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4059. if (line) {
  4060. // put this line at the end of all childs
  4061. frame.removeChild(line);
  4062. frame.appendChild(line);
  4063. }
  4064. else {
  4065. // create the axis line
  4066. line = document.createElement('div');
  4067. line.className = 'grid horizontal major';
  4068. frame.appendChild(line);
  4069. this.dom.line = line;
  4070. }
  4071. line.style.top = this.props.lineTop + 'px';
  4072. }
  4073. else {
  4074. if (line && line.parentElement) {
  4075. frame.removeChild(line.line);
  4076. delete this.dom.line;
  4077. }
  4078. }
  4079. };
  4080. /**
  4081. * Create characters used to determine the size of text on the axis
  4082. * @private
  4083. */
  4084. TimeAxis.prototype._repaintMeasureChars = function () {
  4085. // calculate the width and height of a single character
  4086. // this is used to calculate the step size, and also the positioning of the
  4087. // axis
  4088. var dom = this.dom,
  4089. text;
  4090. if (!dom.measureCharMinor) {
  4091. text = document.createTextNode('0');
  4092. var measureCharMinor = document.createElement('DIV');
  4093. measureCharMinor.className = 'text minor measure';
  4094. measureCharMinor.appendChild(text);
  4095. this.frame.appendChild(measureCharMinor);
  4096. dom.measureCharMinor = measureCharMinor;
  4097. }
  4098. if (!dom.measureCharMajor) {
  4099. text = document.createTextNode('0');
  4100. var measureCharMajor = document.createElement('DIV');
  4101. measureCharMajor.className = 'text major measure';
  4102. measureCharMajor.appendChild(text);
  4103. this.frame.appendChild(measureCharMajor);
  4104. dom.measureCharMajor = measureCharMajor;
  4105. }
  4106. };
  4107. /**
  4108. * Reflow the component
  4109. * @return {Boolean} resized
  4110. */
  4111. TimeAxis.prototype.reflow = function () {
  4112. var changed = 0,
  4113. update = util.updateProperty,
  4114. frame = this.frame,
  4115. range = this.range;
  4116. if (!range) {
  4117. throw new Error('Cannot repaint time axis: no range configured');
  4118. }
  4119. if (frame) {
  4120. changed += update(this, 'top', frame.offsetTop);
  4121. changed += update(this, 'left', frame.offsetLeft);
  4122. // calculate size of a character
  4123. var props = this.props,
  4124. showMinorLabels = this.getOption('showMinorLabels'),
  4125. showMajorLabels = this.getOption('showMajorLabels'),
  4126. measureCharMinor = this.dom.measureCharMinor,
  4127. measureCharMajor = this.dom.measureCharMajor;
  4128. if (measureCharMinor) {
  4129. props.minorCharHeight = measureCharMinor.clientHeight;
  4130. props.minorCharWidth = measureCharMinor.clientWidth;
  4131. }
  4132. if (measureCharMajor) {
  4133. props.majorCharHeight = measureCharMajor.clientHeight;
  4134. props.majorCharWidth = measureCharMajor.clientWidth;
  4135. }
  4136. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4137. if (parentHeight != props.parentHeight) {
  4138. props.parentHeight = parentHeight;
  4139. changed += 1;
  4140. }
  4141. switch (this.getOption('orientation')) {
  4142. case 'bottom':
  4143. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4144. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4145. props.minorLabelTop = 0;
  4146. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4147. props.minorLineTop = -this.top;
  4148. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4149. props.minorLineWidth = 1; // TODO: really calculate width
  4150. props.majorLineTop = -this.top;
  4151. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4152. props.majorLineWidth = 1; // TODO: really calculate width
  4153. props.lineTop = 0;
  4154. break;
  4155. case 'top':
  4156. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4157. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4158. props.majorLabelTop = 0;
  4159. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4160. props.minorLineTop = props.minorLabelTop;
  4161. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4162. props.minorLineWidth = 1; // TODO: really calculate width
  4163. props.majorLineTop = 0;
  4164. props.majorLineHeight = Math.max(parentHeight - this.top);
  4165. props.majorLineWidth = 1; // TODO: really calculate width
  4166. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4167. break;
  4168. default:
  4169. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4170. }
  4171. var height = props.minorLabelHeight + props.majorLabelHeight;
  4172. changed += update(this, 'width', frame.offsetWidth);
  4173. changed += update(this, 'height', height);
  4174. // calculate range and step
  4175. this._updateConversion();
  4176. var start = util.convert(range.start, 'Number'),
  4177. end = util.convert(range.end, 'Number'),
  4178. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4179. -this.toTime(0).valueOf();
  4180. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4181. changed += update(props.range, 'start', start);
  4182. changed += update(props.range, 'end', end);
  4183. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4184. }
  4185. return (changed > 0);
  4186. };
  4187. /**
  4188. * Calculate the scale and offset to convert a position on screen to the
  4189. * corresponding date and vice versa.
  4190. * After the method _updateConversion is executed once, the methods toTime
  4191. * and toScreen can be used.
  4192. * @private
  4193. */
  4194. TimeAxis.prototype._updateConversion = function() {
  4195. var range = this.range;
  4196. if (!range) {
  4197. throw new Error('No range configured');
  4198. }
  4199. if (range.conversion) {
  4200. this.conversion = range.conversion(this.width);
  4201. }
  4202. else {
  4203. this.conversion = Range.conversion(range.start, range.end, this.width);
  4204. }
  4205. };
  4206. /**
  4207. * A current time bar
  4208. * @param {Component} parent
  4209. * @param {Component[]} [depends] Components on which this components depends
  4210. * (except for the parent)
  4211. * @param {Object} [options] Available parameters:
  4212. * {Boolean} [showCurrentTime]
  4213. * @constructor CurrentTime
  4214. * @extends Component
  4215. */
  4216. function CurrentTime (parent, depends, options) {
  4217. this.id = util.randomUUID();
  4218. this.parent = parent;
  4219. this.depends = depends;
  4220. this.options = options || {};
  4221. this.defaultOptions = {
  4222. showCurrentTime: false
  4223. };
  4224. }
  4225. CurrentTime.prototype = new Component();
  4226. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4227. /**
  4228. * Get the container element of the bar, which can be used by a child to
  4229. * add its own widgets.
  4230. * @returns {HTMLElement} container
  4231. */
  4232. CurrentTime.prototype.getContainer = function () {
  4233. return this.frame;
  4234. };
  4235. /**
  4236. * Repaint the component
  4237. * @return {Boolean} changed
  4238. */
  4239. CurrentTime.prototype.repaint = function () {
  4240. var bar = this.frame,
  4241. parent = this.parent,
  4242. parentContainer = parent.parent.getContainer();
  4243. if (!parent) {
  4244. throw new Error('Cannot repaint bar: no parent attached');
  4245. }
  4246. if (!parentContainer) {
  4247. throw new Error('Cannot repaint bar: parent has no container element');
  4248. }
  4249. if (!this.getOption('showCurrentTime')) {
  4250. if (bar) {
  4251. parentContainer.removeChild(bar);
  4252. delete this.frame;
  4253. }
  4254. return;
  4255. }
  4256. if (!bar) {
  4257. bar = document.createElement('div');
  4258. bar.className = 'currenttime';
  4259. bar.style.position = 'absolute';
  4260. bar.style.top = '0px';
  4261. bar.style.height = '100%';
  4262. parentContainer.appendChild(bar);
  4263. this.frame = bar;
  4264. }
  4265. if (!parent.conversion) {
  4266. parent._updateConversion();
  4267. }
  4268. var now = new Date();
  4269. var x = parent.toScreen(now);
  4270. bar.style.left = x + 'px';
  4271. bar.title = 'Current time: ' + now;
  4272. // start a timer to adjust for the new time
  4273. if (this.currentTimeTimer !== undefined) {
  4274. clearTimeout(this.currentTimeTimer);
  4275. delete this.currentTimeTimer;
  4276. }
  4277. var timeline = this;
  4278. var interval = 1 / parent.conversion.scale / 2;
  4279. if (interval < 30) {
  4280. interval = 30;
  4281. }
  4282. this.currentTimeTimer = setTimeout(function() {
  4283. timeline.repaint();
  4284. }, interval);
  4285. return false;
  4286. };
  4287. /**
  4288. * A custom time bar
  4289. * @param {Component} parent
  4290. * @param {Component[]} [depends] Components on which this components depends
  4291. * (except for the parent)
  4292. * @param {Object} [options] Available parameters:
  4293. * {Boolean} [showCustomTime]
  4294. * @constructor CustomTime
  4295. * @extends Component
  4296. */
  4297. function CustomTime (parent, depends, options) {
  4298. this.id = util.randomUUID();
  4299. this.parent = parent;
  4300. this.depends = depends;
  4301. this.options = options || {};
  4302. this.defaultOptions = {
  4303. showCustomTime: false
  4304. };
  4305. this.listeners = [];
  4306. this.customTime = new Date();
  4307. }
  4308. CustomTime.prototype = new Component();
  4309. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4310. /**
  4311. * Get the container element of the bar, which can be used by a child to
  4312. * add its own widgets.
  4313. * @returns {HTMLElement} container
  4314. */
  4315. CustomTime.prototype.getContainer = function () {
  4316. return this.frame;
  4317. };
  4318. /**
  4319. * Repaint the component
  4320. * @return {Boolean} changed
  4321. */
  4322. CustomTime.prototype.repaint = function () {
  4323. var bar = this.frame,
  4324. parent = this.parent,
  4325. parentContainer = parent.parent.getContainer();
  4326. if (!parent) {
  4327. throw new Error('Cannot repaint bar: no parent attached');
  4328. }
  4329. if (!parentContainer) {
  4330. throw new Error('Cannot repaint bar: parent has no container element');
  4331. }
  4332. if (!this.getOption('showCustomTime')) {
  4333. if (bar) {
  4334. parentContainer.removeChild(bar);
  4335. delete this.frame;
  4336. }
  4337. return;
  4338. }
  4339. if (!bar) {
  4340. bar = document.createElement('div');
  4341. bar.className = 'customtime';
  4342. bar.style.position = 'absolute';
  4343. bar.style.top = '0px';
  4344. bar.style.height = '100%';
  4345. parentContainer.appendChild(bar);
  4346. var drag = document.createElement('div');
  4347. drag.style.position = 'relative';
  4348. drag.style.top = '0px';
  4349. drag.style.left = '-10px';
  4350. drag.style.height = '100%';
  4351. drag.style.width = '20px';
  4352. bar.appendChild(drag);
  4353. this.frame = bar;
  4354. this.subscribe(this, 'movetime');
  4355. }
  4356. if (!parent.conversion) {
  4357. parent._updateConversion();
  4358. }
  4359. var x = parent.toScreen(this.customTime);
  4360. bar.style.left = x + 'px';
  4361. bar.title = 'Time: ' + this.customTime;
  4362. return false;
  4363. };
  4364. /**
  4365. * Set custom time.
  4366. * @param {Date} time
  4367. */
  4368. CustomTime.prototype._setCustomTime = function(time) {
  4369. this.customTime = new Date(time.valueOf());
  4370. this.repaint();
  4371. };
  4372. /**
  4373. * Retrieve the current custom time.
  4374. * @return {Date} customTime
  4375. */
  4376. CustomTime.prototype._getCustomTime = function() {
  4377. return new Date(this.customTime.valueOf());
  4378. };
  4379. /**
  4380. * Add listeners for mouse and touch events to the component
  4381. * @param {Component} component
  4382. */
  4383. CustomTime.prototype.subscribe = function (component, event) {
  4384. var me = this;
  4385. var listener = {
  4386. component: component,
  4387. event: event,
  4388. callback: function (event) {
  4389. me._onMouseDown(event, listener);
  4390. },
  4391. params: {}
  4392. };
  4393. component.on('mousedown', listener.callback);
  4394. me.listeners.push(listener);
  4395. };
  4396. /**
  4397. * Event handler
  4398. * @param {String} event name of the event, for example 'click', 'mousemove'
  4399. * @param {function} callback callback handler, invoked with the raw HTML Event
  4400. * as parameter.
  4401. */
  4402. CustomTime.prototype.on = function (event, callback) {
  4403. var bar = this.frame;
  4404. if (!bar) {
  4405. throw new Error('Cannot add event listener: no parent attached');
  4406. }
  4407. events.addListener(this, event, callback);
  4408. util.addEventListener(bar, event, callback);
  4409. };
  4410. /**
  4411. * Start moving horizontally
  4412. * @param {Event} event
  4413. * @param {Object} listener Listener containing the component and params
  4414. * @private
  4415. */
  4416. CustomTime.prototype._onMouseDown = function(event, listener) {
  4417. event = event || window.event;
  4418. var params = listener.params;
  4419. // only react on left mouse button down
  4420. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  4421. if (!leftButtonDown) {
  4422. return;
  4423. }
  4424. // get mouse position
  4425. params.mouseX = util.getPageX(event);
  4426. params.moved = false;
  4427. params.customTime = this.customTime;
  4428. // add event listeners to handle moving the custom time bar
  4429. var me = this;
  4430. if (!params.onMouseMove) {
  4431. params.onMouseMove = function (event) {
  4432. me._onMouseMove(event, listener);
  4433. };
  4434. util.addEventListener(document, 'mousemove', params.onMouseMove);
  4435. }
  4436. if (!params.onMouseUp) {
  4437. params.onMouseUp = function (event) {
  4438. me._onMouseUp(event, listener);
  4439. };
  4440. util.addEventListener(document, 'mouseup', params.onMouseUp);
  4441. }
  4442. util.stopPropagation(event);
  4443. util.preventDefault(event);
  4444. };
  4445. /**
  4446. * Perform moving operating.
  4447. * This function activated from within the funcion CustomTime._onMouseDown().
  4448. * @param {Event} event
  4449. * @param {Object} listener
  4450. * @private
  4451. */
  4452. CustomTime.prototype._onMouseMove = function (event, listener) {
  4453. event = event || window.event;
  4454. var params = listener.params;
  4455. var parent = this.parent;
  4456. // calculate change in mouse position
  4457. var mouseX = util.getPageX(event);
  4458. if (params.mouseX === undefined) {
  4459. params.mouseX = mouseX;
  4460. }
  4461. var diff = mouseX - params.mouseX;
  4462. // if mouse movement is big enough, register it as a "moved" event
  4463. if (Math.abs(diff) >= 1) {
  4464. params.moved = true;
  4465. }
  4466. var x = parent.toScreen(params.customTime);
  4467. var xnew = x + diff;
  4468. var time = parent.toTime(xnew);
  4469. this._setCustomTime(time);
  4470. // fire a timechange event
  4471. events.trigger(this, 'timechange', {customTime: this.customTime});
  4472. util.preventDefault(event);
  4473. };
  4474. /**
  4475. * Stop moving operating.
  4476. * This function activated from within the function CustomTime._onMouseDown().
  4477. * @param {event} event
  4478. * @param {Object} listener
  4479. * @private
  4480. */
  4481. CustomTime.prototype._onMouseUp = function (event, listener) {
  4482. event = event || window.event;
  4483. var params = listener.params;
  4484. // remove event listeners here, important for Safari
  4485. if (params.onMouseMove) {
  4486. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  4487. params.onMouseMove = null;
  4488. }
  4489. if (params.onMouseUp) {
  4490. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  4491. params.onMouseUp = null;
  4492. }
  4493. if (params.moved) {
  4494. // fire a timechanged event
  4495. events.trigger(this, 'timechanged', {customTime: this.customTime});
  4496. }
  4497. };
  4498. /**
  4499. * An ItemSet holds a set of items and ranges which can be displayed in a
  4500. * range. The width is determined by the parent of the ItemSet, and the height
  4501. * is determined by the size of the items.
  4502. * @param {Component} parent
  4503. * @param {Component[]} [depends] Components on which this components depends
  4504. * (except for the parent)
  4505. * @param {Object} [options] See ItemSet.setOptions for the available
  4506. * options.
  4507. * @constructor ItemSet
  4508. * @extends Panel
  4509. */
  4510. // TODO: improve performance by replacing all Array.forEach with a for loop
  4511. function ItemSet(parent, depends, options) {
  4512. this.id = util.randomUUID();
  4513. this.parent = parent;
  4514. this.depends = depends;
  4515. // one options object is shared by this itemset and all its items
  4516. this.options = options || {};
  4517. this.defaultOptions = {
  4518. type: 'box',
  4519. align: 'center',
  4520. orientation: 'bottom',
  4521. margin: {
  4522. axis: 20,
  4523. item: 10
  4524. },
  4525. padding: 5
  4526. };
  4527. this.dom = {};
  4528. var me = this;
  4529. this.itemsData = null; // DataSet
  4530. this.range = null; // Range or Object {start: number, end: number}
  4531. this.listeners = {
  4532. 'add': function (event, params, senderId) {
  4533. if (senderId != me.id) {
  4534. me._onAdd(params.items);
  4535. }
  4536. },
  4537. 'update': function (event, params, senderId) {
  4538. if (senderId != me.id) {
  4539. me._onUpdate(params.items);
  4540. }
  4541. },
  4542. 'remove': function (event, params, senderId) {
  4543. if (senderId != me.id) {
  4544. me._onRemove(params.items);
  4545. }
  4546. }
  4547. };
  4548. this.items = {}; // object with an Item for every data item
  4549. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4550. this.stack = new Stack(this, Object.create(this.options));
  4551. this.conversion = null;
  4552. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4553. }
  4554. ItemSet.prototype = new Panel();
  4555. // available item types will be registered here
  4556. ItemSet.types = {
  4557. box: ItemBox,
  4558. range: ItemRange,
  4559. rangeoverflow: ItemRangeOverflow,
  4560. point: ItemPoint
  4561. };
  4562. /**
  4563. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4564. * @param {Object} [options] The following options are available:
  4565. * {String | function} [className]
  4566. * class name for the itemset
  4567. * {String} [type]
  4568. * Default type for the items. Choose from 'box'
  4569. * (default), 'point', or 'range'. The default
  4570. * Style can be overwritten by individual items.
  4571. * {String} align
  4572. * Alignment for the items, only applicable for
  4573. * ItemBox. Choose 'center' (default), 'left', or
  4574. * 'right'.
  4575. * {String} orientation
  4576. * Orientation of the item set. Choose 'top' or
  4577. * 'bottom' (default).
  4578. * {Number} margin.axis
  4579. * Margin between the axis and the items in pixels.
  4580. * Default is 20.
  4581. * {Number} margin.item
  4582. * Margin between items in pixels. Default is 10.
  4583. * {Number} padding
  4584. * Padding of the contents of an item in pixels.
  4585. * Must correspond with the items css. Default is 5.
  4586. */
  4587. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4588. /**
  4589. * Set range (start and end).
  4590. * @param {Range | Object} range A Range or an object containing start and end.
  4591. */
  4592. ItemSet.prototype.setRange = function setRange(range) {
  4593. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4594. throw new TypeError('Range must be an instance of Range, ' +
  4595. 'or an object containing start and end.');
  4596. }
  4597. this.range = range;
  4598. };
  4599. /**
  4600. * Repaint the component
  4601. * @return {Boolean} changed
  4602. */
  4603. ItemSet.prototype.repaint = function repaint() {
  4604. var changed = 0,
  4605. update = util.updateProperty,
  4606. asSize = util.option.asSize,
  4607. options = this.options,
  4608. orientation = this.getOption('orientation'),
  4609. defaultOptions = this.defaultOptions,
  4610. frame = this.frame;
  4611. if (!frame) {
  4612. frame = document.createElement('div');
  4613. frame.className = 'itemset';
  4614. var className = options.className;
  4615. if (className) {
  4616. util.addClassName(frame, util.option.asString(className));
  4617. }
  4618. // create background panel
  4619. var background = document.createElement('div');
  4620. background.className = 'background';
  4621. frame.appendChild(background);
  4622. this.dom.background = background;
  4623. // create foreground panel
  4624. var foreground = document.createElement('div');
  4625. foreground.className = 'foreground';
  4626. frame.appendChild(foreground);
  4627. this.dom.foreground = foreground;
  4628. // create axis panel
  4629. var axis = document.createElement('div');
  4630. axis.className = 'itemset-axis';
  4631. //frame.appendChild(axis);
  4632. this.dom.axis = axis;
  4633. this.frame = frame;
  4634. changed += 1;
  4635. }
  4636. if (!this.parent) {
  4637. throw new Error('Cannot repaint itemset: no parent attached');
  4638. }
  4639. var parentContainer = this.parent.getContainer();
  4640. if (!parentContainer) {
  4641. throw new Error('Cannot repaint itemset: parent has no container element');
  4642. }
  4643. if (!frame.parentNode) {
  4644. parentContainer.appendChild(frame);
  4645. changed += 1;
  4646. }
  4647. if (!this.dom.axis.parentNode) {
  4648. parentContainer.appendChild(this.dom.axis);
  4649. changed += 1;
  4650. }
  4651. // reposition frame
  4652. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4653. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4654. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4655. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4656. // reposition axis
  4657. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4658. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4659. if (orientation == 'bottom') {
  4660. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4661. }
  4662. else { // orientation == 'top'
  4663. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4664. }
  4665. this._updateConversion();
  4666. var me = this,
  4667. queue = this.queue,
  4668. itemsData = this.itemsData,
  4669. items = this.items,
  4670. dataOptions = {
  4671. // TODO: cleanup
  4672. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  4673. };
  4674. // show/hide added/changed/removed items
  4675. Object.keys(queue).forEach(function (id) {
  4676. //var entry = queue[id];
  4677. var action = queue[id];
  4678. var item = items[id];
  4679. //var item = entry.item;
  4680. //noinspection FallthroughInSwitchStatementJS
  4681. switch (action) {
  4682. case 'add':
  4683. case 'update':
  4684. var itemData = itemsData && itemsData.get(id, dataOptions);
  4685. if (itemData) {
  4686. var type = itemData.type ||
  4687. (itemData.start && itemData.end && 'range') ||
  4688. options.type ||
  4689. 'box';
  4690. var constructor = ItemSet.types[type];
  4691. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4692. if (item) {
  4693. // update item
  4694. if (!constructor || !(item instanceof constructor)) {
  4695. // item type has changed, hide and delete the item
  4696. changed += item.hide();
  4697. item = null;
  4698. }
  4699. else {
  4700. item.data = itemData; // TODO: create a method item.setData ?
  4701. changed++;
  4702. }
  4703. }
  4704. if (!item) {
  4705. // create item
  4706. if (constructor) {
  4707. item = new constructor(me, itemData, options, defaultOptions);
  4708. changed++;
  4709. }
  4710. else {
  4711. throw new TypeError('Unknown item type "' + type + '"');
  4712. }
  4713. }
  4714. // force a repaint (not only a reposition)
  4715. item.repaint();
  4716. items[id] = item;
  4717. }
  4718. // update queue
  4719. delete queue[id];
  4720. break;
  4721. case 'remove':
  4722. if (item) {
  4723. // remove DOM of the item
  4724. changed += item.hide();
  4725. }
  4726. // update lists
  4727. delete items[id];
  4728. delete queue[id];
  4729. break;
  4730. default:
  4731. console.log('Error: unknown action "' + action + '"');
  4732. }
  4733. });
  4734. // reposition all items. Show items only when in the visible area
  4735. util.forEach(this.items, function (item) {
  4736. if (item.visible) {
  4737. changed += item.show();
  4738. item.reposition();
  4739. }
  4740. else {
  4741. changed += item.hide();
  4742. }
  4743. });
  4744. return (changed > 0);
  4745. };
  4746. /**
  4747. * Get the foreground container element
  4748. * @return {HTMLElement} foreground
  4749. */
  4750. ItemSet.prototype.getForeground = function getForeground() {
  4751. return this.dom.foreground;
  4752. };
  4753. /**
  4754. * Get the background container element
  4755. * @return {HTMLElement} background
  4756. */
  4757. ItemSet.prototype.getBackground = function getBackground() {
  4758. return this.dom.background;
  4759. };
  4760. /**
  4761. * Get the axis container element
  4762. * @return {HTMLElement} axis
  4763. */
  4764. ItemSet.prototype.getAxis = function getAxis() {
  4765. return this.dom.axis;
  4766. };
  4767. /**
  4768. * Reflow the component
  4769. * @return {Boolean} resized
  4770. */
  4771. ItemSet.prototype.reflow = function reflow () {
  4772. var changed = 0,
  4773. options = this.options,
  4774. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4775. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4776. update = util.updateProperty,
  4777. asNumber = util.option.asNumber,
  4778. asSize = util.option.asSize,
  4779. frame = this.frame;
  4780. if (frame) {
  4781. this._updateConversion();
  4782. util.forEach(this.items, function (item) {
  4783. changed += item.reflow();
  4784. });
  4785. // TODO: stack.update should be triggered via an event, in stack itself
  4786. // TODO: only update the stack when there are changed items
  4787. this.stack.update();
  4788. var maxHeight = asNumber(options.maxHeight);
  4789. var fixedHeight = (asSize(options.height) != null);
  4790. var height;
  4791. if (fixedHeight) {
  4792. height = frame.offsetHeight;
  4793. }
  4794. else {
  4795. // height is not specified, determine the height from the height and positioned items
  4796. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4797. if (visibleItems.length) {
  4798. var min = visibleItems[0].top;
  4799. var max = visibleItems[0].top + visibleItems[0].height;
  4800. util.forEach(visibleItems, function (item) {
  4801. min = Math.min(min, item.top);
  4802. max = Math.max(max, (item.top + item.height));
  4803. });
  4804. height = (max - min) + marginAxis + marginItem;
  4805. }
  4806. else {
  4807. height = marginAxis + marginItem;
  4808. }
  4809. }
  4810. if (maxHeight != null) {
  4811. height = Math.min(height, maxHeight);
  4812. }
  4813. changed += update(this, 'height', height);
  4814. // calculate height from items
  4815. changed += update(this, 'top', frame.offsetTop);
  4816. changed += update(this, 'left', frame.offsetLeft);
  4817. changed += update(this, 'width', frame.offsetWidth);
  4818. }
  4819. else {
  4820. changed += 1;
  4821. }
  4822. return (changed > 0);
  4823. };
  4824. /**
  4825. * Hide this component from the DOM
  4826. * @return {Boolean} changed
  4827. */
  4828. ItemSet.prototype.hide = function hide() {
  4829. var changed = false;
  4830. // remove the DOM
  4831. if (this.frame && this.frame.parentNode) {
  4832. this.frame.parentNode.removeChild(this.frame);
  4833. changed = true;
  4834. }
  4835. if (this.dom.axis && this.dom.axis.parentNode) {
  4836. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4837. changed = true;
  4838. }
  4839. return changed;
  4840. };
  4841. /**
  4842. * Set items
  4843. * @param {vis.DataSet | null} items
  4844. */
  4845. ItemSet.prototype.setItems = function setItems(items) {
  4846. var me = this,
  4847. ids,
  4848. oldItemsData = this.itemsData;
  4849. // replace the dataset
  4850. if (!items) {
  4851. this.itemsData = null;
  4852. }
  4853. else if (items instanceof DataSet || items instanceof DataView) {
  4854. this.itemsData = items;
  4855. }
  4856. else {
  4857. throw new TypeError('Data must be an instance of DataSet');
  4858. }
  4859. if (oldItemsData) {
  4860. // unsubscribe from old dataset
  4861. util.forEach(this.listeners, function (callback, event) {
  4862. oldItemsData.unsubscribe(event, callback);
  4863. });
  4864. // remove all drawn items
  4865. ids = oldItemsData.getIds();
  4866. this._onRemove(ids);
  4867. }
  4868. if (this.itemsData) {
  4869. // subscribe to new dataset
  4870. var id = this.id;
  4871. util.forEach(this.listeners, function (callback, event) {
  4872. me.itemsData.subscribe(event, callback, id);
  4873. });
  4874. // draw all new items
  4875. ids = this.itemsData.getIds();
  4876. this._onAdd(ids);
  4877. }
  4878. };
  4879. /**
  4880. * Get the current items items
  4881. * @returns {vis.DataSet | null}
  4882. */
  4883. ItemSet.prototype.getItems = function getItems() {
  4884. return this.itemsData;
  4885. };
  4886. /**
  4887. * Handle updated items
  4888. * @param {Number[]} ids
  4889. * @private
  4890. */
  4891. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4892. this._toQueue('update', ids);
  4893. };
  4894. /**
  4895. * Handle changed items
  4896. * @param {Number[]} ids
  4897. * @private
  4898. */
  4899. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4900. this._toQueue('add', ids);
  4901. };
  4902. /**
  4903. * Handle removed items
  4904. * @param {Number[]} ids
  4905. * @private
  4906. */
  4907. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4908. this._toQueue('remove', ids);
  4909. };
  4910. /**
  4911. * Put items in the queue to be added/updated/remove
  4912. * @param {String} action can be 'add', 'update', 'remove'
  4913. * @param {Number[]} ids
  4914. */
  4915. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4916. var queue = this.queue;
  4917. ids.forEach(function (id) {
  4918. queue[id] = action;
  4919. });
  4920. if (this.controller) {
  4921. //this.requestReflow();
  4922. this.requestRepaint();
  4923. }
  4924. };
  4925. /**
  4926. * Calculate the scale and offset to convert a position on screen to the
  4927. * corresponding date and vice versa.
  4928. * After the method _updateConversion is executed once, the methods toTime
  4929. * and toScreen can be used.
  4930. * @private
  4931. */
  4932. ItemSet.prototype._updateConversion = function _updateConversion() {
  4933. var range = this.range;
  4934. if (!range) {
  4935. throw new Error('No range configured');
  4936. }
  4937. if (range.conversion) {
  4938. this.conversion = range.conversion(this.width);
  4939. }
  4940. else {
  4941. this.conversion = Range.conversion(range.start, range.end, this.width);
  4942. }
  4943. };
  4944. /**
  4945. * Convert a position on screen (pixels) to a datetime
  4946. * Before this method can be used, the method _updateConversion must be
  4947. * executed once.
  4948. * @param {int} x Position on the screen in pixels
  4949. * @return {Date} time The datetime the corresponds with given position x
  4950. */
  4951. ItemSet.prototype.toTime = function toTime(x) {
  4952. var conversion = this.conversion;
  4953. return new Date(x / conversion.scale + conversion.offset);
  4954. };
  4955. /**
  4956. * Convert a datetime (Date object) into a position on the screen
  4957. * Before this method can be used, the method _updateConversion must be
  4958. * executed once.
  4959. * @param {Date} time A date
  4960. * @return {int} x The position on the screen in pixels which corresponds
  4961. * with the given date.
  4962. */
  4963. ItemSet.prototype.toScreen = function toScreen(time) {
  4964. var conversion = this.conversion;
  4965. return (time.valueOf() - conversion.offset) * conversion.scale;
  4966. };
  4967. /**
  4968. * @constructor Item
  4969. * @param {ItemSet} parent
  4970. * @param {Object} data Object containing (optional) parameters type,
  4971. * start, end, content, group, className.
  4972. * @param {Object} [options] Options to set initial property values
  4973. * @param {Object} [defaultOptions] default options
  4974. * // TODO: describe available options
  4975. */
  4976. function Item (parent, data, options, defaultOptions) {
  4977. this.parent = parent;
  4978. this.data = data;
  4979. this.dom = null;
  4980. this.options = options || {};
  4981. this.defaultOptions = defaultOptions || {};
  4982. this.selected = false;
  4983. this.visible = false;
  4984. this.top = 0;
  4985. this.left = 0;
  4986. this.width = 0;
  4987. this.height = 0;
  4988. }
  4989. /**
  4990. * Select current item
  4991. */
  4992. Item.prototype.select = function select() {
  4993. this.selected = true;
  4994. };
  4995. /**
  4996. * Unselect current item
  4997. */
  4998. Item.prototype.unselect = function unselect() {
  4999. this.selected = false;
  5000. };
  5001. /**
  5002. * Show the Item in the DOM (when not already visible)
  5003. * @return {Boolean} changed
  5004. */
  5005. Item.prototype.show = function show() {
  5006. return false;
  5007. };
  5008. /**
  5009. * Hide the Item from the DOM (when visible)
  5010. * @return {Boolean} changed
  5011. */
  5012. Item.prototype.hide = function hide() {
  5013. return false;
  5014. };
  5015. /**
  5016. * Repaint the item
  5017. * @return {Boolean} changed
  5018. */
  5019. Item.prototype.repaint = function repaint() {
  5020. // should be implemented by the item
  5021. return false;
  5022. };
  5023. /**
  5024. * Reflow the item
  5025. * @return {Boolean} resized
  5026. */
  5027. Item.prototype.reflow = function reflow() {
  5028. // should be implemented by the item
  5029. return false;
  5030. };
  5031. /**
  5032. * Return the items width
  5033. * @return {Integer} width
  5034. */
  5035. Item.prototype.getWidth = function getWidth() {
  5036. return this.width;
  5037. }
  5038. /**
  5039. * @constructor ItemBox
  5040. * @extends Item
  5041. * @param {ItemSet} parent
  5042. * @param {Object} data Object containing parameters start
  5043. * content, className.
  5044. * @param {Object} [options] Options to set initial property values
  5045. * @param {Object} [defaultOptions] default options
  5046. * // TODO: describe available options
  5047. */
  5048. function ItemBox (parent, data, options, defaultOptions) {
  5049. this.props = {
  5050. dot: {
  5051. left: 0,
  5052. top: 0,
  5053. width: 0,
  5054. height: 0
  5055. },
  5056. line: {
  5057. top: 0,
  5058. left: 0,
  5059. width: 0,
  5060. height: 0
  5061. }
  5062. };
  5063. Item.call(this, parent, data, options, defaultOptions);
  5064. }
  5065. ItemBox.prototype = new Item (null, null);
  5066. /**
  5067. * Select the item
  5068. * @override
  5069. */
  5070. ItemBox.prototype.select = function select() {
  5071. this.selected = true;
  5072. // TODO: select and unselect
  5073. };
  5074. /**
  5075. * Unselect the item
  5076. * @override
  5077. */
  5078. ItemBox.prototype.unselect = function unselect() {
  5079. this.selected = false;
  5080. // TODO: select and unselect
  5081. };
  5082. /**
  5083. * Repaint the item
  5084. * @return {Boolean} changed
  5085. */
  5086. ItemBox.prototype.repaint = function repaint() {
  5087. // TODO: make an efficient repaint
  5088. var changed = false;
  5089. var dom = this.dom;
  5090. if (!dom) {
  5091. this._create();
  5092. dom = this.dom;
  5093. changed = true;
  5094. }
  5095. if (dom) {
  5096. if (!this.parent) {
  5097. throw new Error('Cannot repaint item: no parent attached');
  5098. }
  5099. if (!dom.box.parentNode) {
  5100. var foreground = this.parent.getForeground();
  5101. if (!foreground) {
  5102. throw new Error('Cannot repaint time axis: ' +
  5103. 'parent has no foreground container element');
  5104. }
  5105. foreground.appendChild(dom.box);
  5106. changed = true;
  5107. }
  5108. if (!dom.line.parentNode) {
  5109. var background = this.parent.getBackground();
  5110. if (!background) {
  5111. throw new Error('Cannot repaint time axis: ' +
  5112. 'parent has no background container element');
  5113. }
  5114. background.appendChild(dom.line);
  5115. changed = true;
  5116. }
  5117. if (!dom.dot.parentNode) {
  5118. var axis = this.parent.getAxis();
  5119. if (!background) {
  5120. throw new Error('Cannot repaint time axis: ' +
  5121. 'parent has no axis container element');
  5122. }
  5123. axis.appendChild(dom.dot);
  5124. changed = true;
  5125. }
  5126. // update contents
  5127. if (this.data.content != this.content) {
  5128. this.content = this.data.content;
  5129. if (this.content instanceof Element) {
  5130. dom.content.innerHTML = '';
  5131. dom.content.appendChild(this.content);
  5132. }
  5133. else if (this.data.content != undefined) {
  5134. dom.content.innerHTML = this.content;
  5135. }
  5136. else {
  5137. throw new Error('Property "content" missing in item ' + this.data.id);
  5138. }
  5139. changed = true;
  5140. }
  5141. // update class
  5142. var className = (this.data.className? ' ' + this.data.className : '') +
  5143. (this.selected ? ' selected' : '');
  5144. if (this.className != className) {
  5145. this.className = className;
  5146. dom.box.className = 'item box' + className;
  5147. dom.line.className = 'item line' + className;
  5148. dom.dot.className = 'item dot' + className;
  5149. changed = true;
  5150. }
  5151. }
  5152. return changed;
  5153. };
  5154. /**
  5155. * Show the item in the DOM (when not already visible). The items DOM will
  5156. * be created when needed.
  5157. * @return {Boolean} changed
  5158. */
  5159. ItemBox.prototype.show = function show() {
  5160. if (!this.dom || !this.dom.box.parentNode) {
  5161. return this.repaint();
  5162. }
  5163. else {
  5164. return false;
  5165. }
  5166. };
  5167. /**
  5168. * Hide the item from the DOM (when visible)
  5169. * @return {Boolean} changed
  5170. */
  5171. ItemBox.prototype.hide = function hide() {
  5172. var changed = false,
  5173. dom = this.dom;
  5174. if (dom) {
  5175. if (dom.box.parentNode) {
  5176. dom.box.parentNode.removeChild(dom.box);
  5177. changed = true;
  5178. }
  5179. if (dom.line.parentNode) {
  5180. dom.line.parentNode.removeChild(dom.line);
  5181. }
  5182. if (dom.dot.parentNode) {
  5183. dom.dot.parentNode.removeChild(dom.dot);
  5184. }
  5185. }
  5186. return changed;
  5187. };
  5188. /**
  5189. * Reflow the item: calculate its actual size and position from the DOM
  5190. * @return {boolean} resized returns true if the axis is resized
  5191. * @override
  5192. */
  5193. ItemBox.prototype.reflow = function reflow() {
  5194. var changed = 0,
  5195. update,
  5196. dom,
  5197. props,
  5198. options,
  5199. margin,
  5200. start,
  5201. align,
  5202. orientation,
  5203. top,
  5204. left,
  5205. data,
  5206. range;
  5207. if (this.data.start == undefined) {
  5208. throw new Error('Property "start" missing in item ' + this.data.id);
  5209. }
  5210. data = this.data;
  5211. range = this.parent && this.parent.range;
  5212. if (data && range) {
  5213. // TODO: account for the width of the item
  5214. var interval = (range.end - range.start);
  5215. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5216. }
  5217. else {
  5218. this.visible = false;
  5219. }
  5220. if (this.visible) {
  5221. dom = this.dom;
  5222. if (dom) {
  5223. update = util.updateProperty;
  5224. props = this.props;
  5225. options = this.options;
  5226. start = this.parent.toScreen(this.data.start);
  5227. align = options.align || this.defaultOptions.align;
  5228. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5229. orientation = options.orientation || this.defaultOptions.orientation;
  5230. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5231. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5232. changed += update(props.line, 'width', dom.line.offsetWidth);
  5233. changed += update(props.line, 'height', dom.line.offsetHeight);
  5234. changed += update(props.line, 'top', dom.line.offsetTop);
  5235. changed += update(this, 'width', dom.box.offsetWidth);
  5236. changed += update(this, 'height', dom.box.offsetHeight);
  5237. if (align == 'right') {
  5238. left = start - this.width;
  5239. }
  5240. else if (align == 'left') {
  5241. left = start;
  5242. }
  5243. else {
  5244. // default or 'center'
  5245. left = start - this.width / 2;
  5246. }
  5247. changed += update(this, 'left', left);
  5248. changed += update(props.line, 'left', start - props.line.width / 2);
  5249. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5250. changed += update(props.dot, 'top', -props.dot.height / 2);
  5251. if (orientation == 'top') {
  5252. top = margin;
  5253. changed += update(this, 'top', top);
  5254. }
  5255. else {
  5256. // default or 'bottom'
  5257. var parentHeight = this.parent.height;
  5258. top = parentHeight - this.height - margin;
  5259. changed += update(this, 'top', top);
  5260. }
  5261. }
  5262. else {
  5263. changed += 1;
  5264. }
  5265. }
  5266. return (changed > 0);
  5267. };
  5268. /**
  5269. * Create an items DOM
  5270. * @private
  5271. */
  5272. ItemBox.prototype._create = function _create() {
  5273. var dom = this.dom;
  5274. if (!dom) {
  5275. this.dom = dom = {};
  5276. // create the box
  5277. dom.box = document.createElement('DIV');
  5278. // className is updated in repaint()
  5279. // contents box (inside the background box). used for making margins
  5280. dom.content = document.createElement('DIV');
  5281. dom.content.className = 'content';
  5282. dom.box.appendChild(dom.content);
  5283. // line to axis
  5284. dom.line = document.createElement('DIV');
  5285. dom.line.className = 'line';
  5286. // dot on axis
  5287. dom.dot = document.createElement('DIV');
  5288. dom.dot.className = 'dot';
  5289. }
  5290. };
  5291. /**
  5292. * Reposition the item, recalculate its left, top, and width, using the current
  5293. * range and size of the items itemset
  5294. * @override
  5295. */
  5296. ItemBox.prototype.reposition = function reposition() {
  5297. var dom = this.dom,
  5298. props = this.props,
  5299. orientation = this.options.orientation || this.defaultOptions.orientation;
  5300. if (dom) {
  5301. var box = dom.box,
  5302. line = dom.line,
  5303. dot = dom.dot;
  5304. box.style.left = this.left + 'px';
  5305. box.style.top = this.top + 'px';
  5306. line.style.left = props.line.left + 'px';
  5307. if (orientation == 'top') {
  5308. line.style.top = 0 + 'px';
  5309. line.style.height = this.top + 'px';
  5310. }
  5311. else {
  5312. // orientation 'bottom'
  5313. line.style.top = (this.top + this.height) + 'px';
  5314. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5315. this.props.dot.height / 2, 0) + 'px';
  5316. }
  5317. dot.style.left = props.dot.left + 'px';
  5318. dot.style.top = props.dot.top + 'px';
  5319. }
  5320. };
  5321. /**
  5322. * @constructor ItemPoint
  5323. * @extends Item
  5324. * @param {ItemSet} parent
  5325. * @param {Object} data Object containing parameters start
  5326. * content, className.
  5327. * @param {Object} [options] Options to set initial property values
  5328. * @param {Object} [defaultOptions] default options
  5329. * // TODO: describe available options
  5330. */
  5331. function ItemPoint (parent, data, options, defaultOptions) {
  5332. this.props = {
  5333. dot: {
  5334. top: 0,
  5335. width: 0,
  5336. height: 0
  5337. },
  5338. content: {
  5339. height: 0,
  5340. marginLeft: 0
  5341. }
  5342. };
  5343. Item.call(this, parent, data, options, defaultOptions);
  5344. }
  5345. ItemPoint.prototype = new Item (null, null);
  5346. /**
  5347. * Select the item
  5348. * @override
  5349. */
  5350. ItemPoint.prototype.select = function select() {
  5351. this.selected = true;
  5352. // TODO: select and unselect
  5353. };
  5354. /**
  5355. * Unselect the item
  5356. * @override
  5357. */
  5358. ItemPoint.prototype.unselect = function unselect() {
  5359. this.selected = false;
  5360. // TODO: select and unselect
  5361. };
  5362. /**
  5363. * Repaint the item
  5364. * @return {Boolean} changed
  5365. */
  5366. ItemPoint.prototype.repaint = function repaint() {
  5367. // TODO: make an efficient repaint
  5368. var changed = false;
  5369. var dom = this.dom;
  5370. if (!dom) {
  5371. this._create();
  5372. dom = this.dom;
  5373. changed = true;
  5374. }
  5375. if (dom) {
  5376. if (!this.parent) {
  5377. throw new Error('Cannot repaint item: no parent attached');
  5378. }
  5379. var foreground = this.parent.getForeground();
  5380. if (!foreground) {
  5381. throw new Error('Cannot repaint time axis: ' +
  5382. 'parent has no foreground container element');
  5383. }
  5384. if (!dom.point.parentNode) {
  5385. foreground.appendChild(dom.point);
  5386. foreground.appendChild(dom.point);
  5387. changed = true;
  5388. }
  5389. // update contents
  5390. if (this.data.content != this.content) {
  5391. this.content = this.data.content;
  5392. if (this.content instanceof Element) {
  5393. dom.content.innerHTML = '';
  5394. dom.content.appendChild(this.content);
  5395. }
  5396. else if (this.data.content != undefined) {
  5397. dom.content.innerHTML = this.content;
  5398. }
  5399. else {
  5400. throw new Error('Property "content" missing in item ' + this.data.id);
  5401. }
  5402. changed = true;
  5403. }
  5404. // update class
  5405. var className = (this.data.className? ' ' + this.data.className : '') +
  5406. (this.selected ? ' selected' : '');
  5407. if (this.className != className) {
  5408. this.className = className;
  5409. dom.point.className = 'item point' + className;
  5410. changed = true;
  5411. }
  5412. }
  5413. return changed;
  5414. };
  5415. /**
  5416. * Show the item in the DOM (when not already visible). The items DOM will
  5417. * be created when needed.
  5418. * @return {Boolean} changed
  5419. */
  5420. ItemPoint.prototype.show = function show() {
  5421. if (!this.dom || !this.dom.point.parentNode) {
  5422. return this.repaint();
  5423. }
  5424. else {
  5425. return false;
  5426. }
  5427. };
  5428. /**
  5429. * Hide the item from the DOM (when visible)
  5430. * @return {Boolean} changed
  5431. */
  5432. ItemPoint.prototype.hide = function hide() {
  5433. var changed = false,
  5434. dom = this.dom;
  5435. if (dom) {
  5436. if (dom.point.parentNode) {
  5437. dom.point.parentNode.removeChild(dom.point);
  5438. changed = true;
  5439. }
  5440. }
  5441. return changed;
  5442. };
  5443. /**
  5444. * Reflow the item: calculate its actual size from the DOM
  5445. * @return {boolean} resized returns true if the axis is resized
  5446. * @override
  5447. */
  5448. ItemPoint.prototype.reflow = function reflow() {
  5449. var changed = 0,
  5450. update,
  5451. dom,
  5452. props,
  5453. options,
  5454. margin,
  5455. orientation,
  5456. start,
  5457. top,
  5458. data,
  5459. range;
  5460. if (this.data.start == undefined) {
  5461. throw new Error('Property "start" missing in item ' + this.data.id);
  5462. }
  5463. data = this.data;
  5464. range = this.parent && this.parent.range;
  5465. if (data && range) {
  5466. // TODO: account for the width of the item
  5467. var interval = (range.end - range.start);
  5468. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5469. }
  5470. else {
  5471. this.visible = false;
  5472. }
  5473. if (this.visible) {
  5474. dom = this.dom;
  5475. if (dom) {
  5476. update = util.updateProperty;
  5477. props = this.props;
  5478. options = this.options;
  5479. orientation = options.orientation || this.defaultOptions.orientation;
  5480. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5481. start = this.parent.toScreen(this.data.start);
  5482. changed += update(this, 'width', dom.point.offsetWidth);
  5483. changed += update(this, 'height', dom.point.offsetHeight);
  5484. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5485. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5486. changed += update(props.content, 'height', dom.content.offsetHeight);
  5487. if (orientation == 'top') {
  5488. top = margin;
  5489. }
  5490. else {
  5491. // default or 'bottom'
  5492. var parentHeight = this.parent.height;
  5493. top = Math.max(parentHeight - this.height - margin, 0);
  5494. }
  5495. changed += update(this, 'top', top);
  5496. changed += update(this, 'left', start - props.dot.width / 2);
  5497. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5498. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5499. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5500. }
  5501. else {
  5502. changed += 1;
  5503. }
  5504. }
  5505. return (changed > 0);
  5506. };
  5507. /**
  5508. * Create an items DOM
  5509. * @private
  5510. */
  5511. ItemPoint.prototype._create = function _create() {
  5512. var dom = this.dom;
  5513. if (!dom) {
  5514. this.dom = dom = {};
  5515. // background box
  5516. dom.point = document.createElement('div');
  5517. // className is updated in repaint()
  5518. // contents box, right from the dot
  5519. dom.content = document.createElement('div');
  5520. dom.content.className = 'content';
  5521. dom.point.appendChild(dom.content);
  5522. // dot at start
  5523. dom.dot = document.createElement('div');
  5524. dom.dot.className = 'dot';
  5525. dom.point.appendChild(dom.dot);
  5526. }
  5527. };
  5528. /**
  5529. * Reposition the item, recalculate its left, top, and width, using the current
  5530. * range and size of the items itemset
  5531. * @override
  5532. */
  5533. ItemPoint.prototype.reposition = function reposition() {
  5534. var dom = this.dom,
  5535. props = this.props;
  5536. if (dom) {
  5537. dom.point.style.top = this.top + 'px';
  5538. dom.point.style.left = this.left + 'px';
  5539. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5540. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5541. dom.dot.style.top = props.dot.top + 'px';
  5542. }
  5543. };
  5544. /**
  5545. * @constructor ItemRange
  5546. * @extends Item
  5547. * @param {ItemSet} parent
  5548. * @param {Object} data Object containing parameters start, end
  5549. * content, className.
  5550. * @param {Object} [options] Options to set initial property values
  5551. * @param {Object} [defaultOptions] default options
  5552. * // TODO: describe available options
  5553. */
  5554. function ItemRange (parent, data, options, defaultOptions) {
  5555. this.props = {
  5556. content: {
  5557. left: 0,
  5558. width: 0
  5559. }
  5560. };
  5561. Item.call(this, parent, data, options, defaultOptions);
  5562. }
  5563. ItemRange.prototype = new Item (null, null);
  5564. /**
  5565. * Select the item
  5566. * @override
  5567. */
  5568. ItemRange.prototype.select = function select() {
  5569. this.selected = true;
  5570. // TODO: select and unselect
  5571. };
  5572. /**
  5573. * Unselect the item
  5574. * @override
  5575. */
  5576. ItemRange.prototype.unselect = function unselect() {
  5577. this.selected = false;
  5578. // TODO: select and unselect
  5579. };
  5580. /**
  5581. * Repaint the item
  5582. * @return {Boolean} changed
  5583. */
  5584. ItemRange.prototype.repaint = function repaint() {
  5585. // TODO: make an efficient repaint
  5586. var changed = false;
  5587. var dom = this.dom;
  5588. if (!dom) {
  5589. this._create();
  5590. dom = this.dom;
  5591. changed = true;
  5592. }
  5593. if (dom) {
  5594. if (!this.parent) {
  5595. throw new Error('Cannot repaint item: no parent attached');
  5596. }
  5597. var foreground = this.parent.getForeground();
  5598. if (!foreground) {
  5599. throw new Error('Cannot repaint time axis: ' +
  5600. 'parent has no foreground container element');
  5601. }
  5602. if (!dom.box.parentNode) {
  5603. foreground.appendChild(dom.box);
  5604. changed = true;
  5605. }
  5606. // update content
  5607. if (this.data.content != this.content) {
  5608. this.content = this.data.content;
  5609. if (this.content instanceof Element) {
  5610. dom.content.innerHTML = '';
  5611. dom.content.appendChild(this.content);
  5612. }
  5613. else if (this.data.content != undefined) {
  5614. dom.content.innerHTML = this.content;
  5615. }
  5616. else {
  5617. throw new Error('Property "content" missing in item ' + this.data.id);
  5618. }
  5619. changed = true;
  5620. }
  5621. // update class
  5622. var className = this.data.className ? (' ' + this.data.className) : '';
  5623. if (this.className != className) {
  5624. this.className = className;
  5625. dom.box.className = 'item range' + className;
  5626. changed = true;
  5627. }
  5628. }
  5629. return changed;
  5630. };
  5631. /**
  5632. * Show the item in the DOM (when not already visible). The items DOM will
  5633. * be created when needed.
  5634. * @return {Boolean} changed
  5635. */
  5636. ItemRange.prototype.show = function show() {
  5637. if (!this.dom || !this.dom.box.parentNode) {
  5638. return this.repaint();
  5639. }
  5640. else {
  5641. return false;
  5642. }
  5643. };
  5644. /**
  5645. * Hide the item from the DOM (when visible)
  5646. * @return {Boolean} changed
  5647. */
  5648. ItemRange.prototype.hide = function hide() {
  5649. var changed = false,
  5650. dom = this.dom;
  5651. if (dom) {
  5652. if (dom.box.parentNode) {
  5653. dom.box.parentNode.removeChild(dom.box);
  5654. changed = true;
  5655. }
  5656. }
  5657. return changed;
  5658. };
  5659. /**
  5660. * Reflow the item: calculate its actual size from the DOM
  5661. * @return {boolean} resized returns true if the axis is resized
  5662. * @override
  5663. */
  5664. ItemRange.prototype.reflow = function reflow() {
  5665. var changed = 0,
  5666. dom,
  5667. props,
  5668. options,
  5669. margin,
  5670. padding,
  5671. parent,
  5672. start,
  5673. end,
  5674. data,
  5675. range,
  5676. update,
  5677. box,
  5678. parentWidth,
  5679. contentLeft,
  5680. orientation,
  5681. top;
  5682. if (this.data.start == undefined) {
  5683. throw new Error('Property "start" missing in item ' + this.data.id);
  5684. }
  5685. if (this.data.end == undefined) {
  5686. throw new Error('Property "end" missing in item ' + this.data.id);
  5687. }
  5688. data = this.data;
  5689. range = this.parent && this.parent.range;
  5690. if (data && range) {
  5691. // TODO: account for the width of the item. Take some margin
  5692. this.visible = (data.start < range.end) && (data.end > range.start);
  5693. }
  5694. else {
  5695. this.visible = false;
  5696. }
  5697. if (this.visible) {
  5698. dom = this.dom;
  5699. if (dom) {
  5700. props = this.props;
  5701. options = this.options;
  5702. parent = this.parent;
  5703. start = parent.toScreen(this.data.start);
  5704. end = parent.toScreen(this.data.end);
  5705. update = util.updateProperty;
  5706. box = dom.box;
  5707. parentWidth = parent.width;
  5708. orientation = options.orientation || this.defaultOptions.orientation;
  5709. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5710. padding = options.padding || this.defaultOptions.padding;
  5711. changed += update(props.content, 'width', dom.content.offsetWidth);
  5712. changed += update(this, 'height', box.offsetHeight);
  5713. // limit the width of the this, as browsers cannot draw very wide divs
  5714. if (start < -parentWidth) {
  5715. start = -parentWidth;
  5716. }
  5717. if (end > 2 * parentWidth) {
  5718. end = 2 * parentWidth;
  5719. }
  5720. // when range exceeds left of the window, position the contents at the left of the visible area
  5721. if (start < 0) {
  5722. contentLeft = Math.min(-start,
  5723. (end - start - props.content.width - 2 * padding));
  5724. // TODO: remove the need for options.padding. it's terrible.
  5725. }
  5726. else {
  5727. contentLeft = 0;
  5728. }
  5729. changed += update(props.content, 'left', contentLeft);
  5730. if (orientation == 'top') {
  5731. top = margin;
  5732. changed += update(this, 'top', top);
  5733. }
  5734. else {
  5735. // default or 'bottom'
  5736. top = parent.height - this.height - margin;
  5737. changed += update(this, 'top', top);
  5738. }
  5739. changed += update(this, 'left', start);
  5740. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5741. }
  5742. else {
  5743. changed += 1;
  5744. }
  5745. }
  5746. return (changed > 0);
  5747. };
  5748. /**
  5749. * Create an items DOM
  5750. * @private
  5751. */
  5752. ItemRange.prototype._create = function _create() {
  5753. var dom = this.dom;
  5754. if (!dom) {
  5755. this.dom = dom = {};
  5756. // background box
  5757. dom.box = document.createElement('div');
  5758. // className is updated in repaint()
  5759. // contents box
  5760. dom.content = document.createElement('div');
  5761. dom.content.className = 'content';
  5762. dom.box.appendChild(dom.content);
  5763. }
  5764. };
  5765. /**
  5766. * Reposition the item, recalculate its left, top, and width, using the current
  5767. * range and size of the items itemset
  5768. * @override
  5769. */
  5770. ItemRange.prototype.reposition = function reposition() {
  5771. var dom = this.dom,
  5772. props = this.props;
  5773. if (dom) {
  5774. dom.box.style.top = this.top + 'px';
  5775. dom.box.style.left = this.left + 'px';
  5776. dom.box.style.width = this.width + 'px';
  5777. dom.content.style.left = props.content.left + 'px';
  5778. }
  5779. };
  5780. /**
  5781. * @constructor ItemRangeOverflow
  5782. * @extends ItemRange
  5783. * @param {ItemSet} parent
  5784. * @param {Object} data Object containing parameters start, end
  5785. * content, className.
  5786. * @param {Object} [options] Options to set initial property values
  5787. * @param {Object} [defaultOptions] default options
  5788. * // TODO: describe available options
  5789. */
  5790. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5791. this.props = {
  5792. content: {
  5793. left: 0,
  5794. width: 0
  5795. }
  5796. };
  5797. ItemRange.call(this, parent, data, options, defaultOptions);
  5798. }
  5799. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5800. /**
  5801. * Repaint the item
  5802. * @return {Boolean} changed
  5803. */
  5804. ItemRangeOverflow.prototype.repaint = function repaint() {
  5805. // TODO: make an efficient repaint
  5806. var changed = false;
  5807. var dom = this.dom;
  5808. if (!dom) {
  5809. this._create();
  5810. dom = this.dom;
  5811. changed = true;
  5812. }
  5813. if (dom) {
  5814. if (!this.parent) {
  5815. throw new Error('Cannot repaint item: no parent attached');
  5816. }
  5817. var foreground = this.parent.getForeground();
  5818. if (!foreground) {
  5819. throw new Error('Cannot repaint time axis: ' +
  5820. 'parent has no foreground container element');
  5821. }
  5822. if (!dom.box.parentNode) {
  5823. foreground.appendChild(dom.box);
  5824. changed = true;
  5825. }
  5826. // update content
  5827. if (this.data.content != this.content) {
  5828. this.content = this.data.content;
  5829. if (this.content instanceof Element) {
  5830. dom.content.innerHTML = '';
  5831. dom.content.appendChild(this.content);
  5832. }
  5833. else if (this.data.content != undefined) {
  5834. dom.content.innerHTML = this.content;
  5835. }
  5836. else {
  5837. throw new Error('Property "content" missing in item ' + this.data.id);
  5838. }
  5839. changed = true;
  5840. }
  5841. // update class
  5842. var className = this.data.className ? (' ' + this.data.className) : '';
  5843. if (this.className != className) {
  5844. this.className = className;
  5845. dom.box.className = 'item rangeoverflow' + className;
  5846. changed = true;
  5847. }
  5848. }
  5849. return changed;
  5850. };
  5851. /**
  5852. * Return the items width
  5853. * @return {Number} width
  5854. */
  5855. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  5856. if (this.props.content !== undefined && this.width < this.props.content.width)
  5857. return this.props.content.width;
  5858. else
  5859. return this.width;
  5860. };
  5861. /**
  5862. * @constructor Group
  5863. * @param {GroupSet} parent
  5864. * @param {Number | String} groupId
  5865. * @param {Object} [options] Options to set initial property values
  5866. * // TODO: describe available options
  5867. * @extends Component
  5868. */
  5869. function Group (parent, groupId, options) {
  5870. this.id = util.randomUUID();
  5871. this.parent = parent;
  5872. this.groupId = groupId;
  5873. this.itemset = null; // ItemSet
  5874. this.options = options || {};
  5875. this.options.top = 0;
  5876. this.props = {
  5877. label: {
  5878. width: 0,
  5879. height: 0
  5880. }
  5881. };
  5882. this.top = 0;
  5883. this.left = 0;
  5884. this.width = 0;
  5885. this.height = 0;
  5886. }
  5887. Group.prototype = new Component();
  5888. // TODO: comment
  5889. Group.prototype.setOptions = Component.prototype.setOptions;
  5890. /**
  5891. * Get the container element of the panel, which can be used by a child to
  5892. * add its own widgets.
  5893. * @returns {HTMLElement} container
  5894. */
  5895. Group.prototype.getContainer = function () {
  5896. return this.parent.getContainer();
  5897. };
  5898. /**
  5899. * Set item set for the group. The group will create a view on the itemset,
  5900. * filtered by the groups id.
  5901. * @param {DataSet | DataView} items
  5902. */
  5903. Group.prototype.setItems = function setItems(items) {
  5904. if (this.itemset) {
  5905. // remove current item set
  5906. this.itemset.hide();
  5907. this.itemset.setItems();
  5908. this.parent.controller.remove(this.itemset);
  5909. this.itemset = null;
  5910. }
  5911. if (items) {
  5912. var groupId = this.groupId;
  5913. var itemsetOptions = Object.create(this.options);
  5914. this.itemset = new ItemSet(this, null, itemsetOptions);
  5915. this.itemset.setRange(this.parent.range);
  5916. this.view = new DataView(items, {
  5917. filter: function (item) {
  5918. return item.group == groupId;
  5919. }
  5920. });
  5921. this.itemset.setItems(this.view);
  5922. this.parent.controller.add(this.itemset);
  5923. }
  5924. };
  5925. /**
  5926. * Repaint the item
  5927. * @return {Boolean} changed
  5928. */
  5929. Group.prototype.repaint = function repaint() {
  5930. return false;
  5931. };
  5932. /**
  5933. * Reflow the item
  5934. * @return {Boolean} resized
  5935. */
  5936. Group.prototype.reflow = function reflow() {
  5937. var changed = 0,
  5938. update = util.updateProperty;
  5939. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5940. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5941. // TODO: reckon with the height of the group label
  5942. if (this.label) {
  5943. var inner = this.label.firstChild;
  5944. changed += update(this.props.label, 'width', inner.clientWidth);
  5945. changed += update(this.props.label, 'height', inner.clientHeight);
  5946. }
  5947. else {
  5948. changed += update(this.props.label, 'width', 0);
  5949. changed += update(this.props.label, 'height', 0);
  5950. }
  5951. return (changed > 0);
  5952. };
  5953. /**
  5954. * An GroupSet holds a set of groups
  5955. * @param {Component} parent
  5956. * @param {Component[]} [depends] Components on which this components depends
  5957. * (except for the parent)
  5958. * @param {Object} [options] See GroupSet.setOptions for the available
  5959. * options.
  5960. * @constructor GroupSet
  5961. * @extends Panel
  5962. */
  5963. function GroupSet(parent, depends, options) {
  5964. this.id = util.randomUUID();
  5965. this.parent = parent;
  5966. this.depends = depends;
  5967. this.options = options || {};
  5968. this.range = null; // Range or Object {start: number, end: number}
  5969. this.itemsData = null; // DataSet with items
  5970. this.groupsData = null; // DataSet with groups
  5971. this.groups = {}; // map with groups
  5972. this.dom = {};
  5973. this.props = {
  5974. labels: {
  5975. width: 0
  5976. }
  5977. };
  5978. // TODO: implement right orientation of the labels
  5979. // changes in groups are queued key/value map containing id/action
  5980. this.queue = {};
  5981. var me = this;
  5982. this.listeners = {
  5983. 'add': function (event, params) {
  5984. me._onAdd(params.items);
  5985. },
  5986. 'update': function (event, params) {
  5987. me._onUpdate(params.items);
  5988. },
  5989. 'remove': function (event, params) {
  5990. me._onRemove(params.items);
  5991. }
  5992. };
  5993. }
  5994. GroupSet.prototype = new Panel();
  5995. /**
  5996. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5997. * @param {Object} [options] The following options are available:
  5998. * {String | function} groupsOrder
  5999. * TODO: describe options
  6000. */
  6001. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  6002. GroupSet.prototype.setRange = function (range) {
  6003. // TODO: implement setRange
  6004. };
  6005. /**
  6006. * Set items
  6007. * @param {vis.DataSet | null} items
  6008. */
  6009. GroupSet.prototype.setItems = function setItems(items) {
  6010. this.itemsData = items;
  6011. for (var id in this.groups) {
  6012. if (this.groups.hasOwnProperty(id)) {
  6013. var group = this.groups[id];
  6014. group.setItems(items);
  6015. }
  6016. }
  6017. };
  6018. /**
  6019. * Get items
  6020. * @return {vis.DataSet | null} items
  6021. */
  6022. GroupSet.prototype.getItems = function getItems() {
  6023. return this.itemsData;
  6024. };
  6025. /**
  6026. * Set range (start and end).
  6027. * @param {Range | Object} range A Range or an object containing start and end.
  6028. */
  6029. GroupSet.prototype.setRange = function setRange(range) {
  6030. this.range = range;
  6031. };
  6032. /**
  6033. * Set groups
  6034. * @param {vis.DataSet} groups
  6035. */
  6036. GroupSet.prototype.setGroups = function setGroups(groups) {
  6037. var me = this,
  6038. ids;
  6039. // unsubscribe from current dataset
  6040. if (this.groupsData) {
  6041. util.forEach(this.listeners, function (callback, event) {
  6042. me.groupsData.unsubscribe(event, callback);
  6043. });
  6044. // remove all drawn groups
  6045. ids = this.groupsData.getIds();
  6046. this._onRemove(ids);
  6047. }
  6048. // replace the dataset
  6049. if (!groups) {
  6050. this.groupsData = null;
  6051. }
  6052. else if (groups instanceof DataSet) {
  6053. this.groupsData = groups;
  6054. }
  6055. else {
  6056. this.groupsData = new DataSet({
  6057. convert: {
  6058. start: 'Date',
  6059. end: 'Date'
  6060. }
  6061. });
  6062. this.groupsData.add(groups);
  6063. }
  6064. if (this.groupsData) {
  6065. // subscribe to new dataset
  6066. var id = this.id;
  6067. util.forEach(this.listeners, function (callback, event) {
  6068. me.groupsData.subscribe(event, callback, id);
  6069. });
  6070. // draw all new groups
  6071. ids = this.groupsData.getIds();
  6072. this._onAdd(ids);
  6073. }
  6074. };
  6075. /**
  6076. * Get groups
  6077. * @return {vis.DataSet | null} groups
  6078. */
  6079. GroupSet.prototype.getGroups = function getGroups() {
  6080. return this.groupsData;
  6081. };
  6082. /**
  6083. * Repaint the component
  6084. * @return {Boolean} changed
  6085. */
  6086. GroupSet.prototype.repaint = function repaint() {
  6087. var changed = 0,
  6088. i, id, group, label,
  6089. update = util.updateProperty,
  6090. asSize = util.option.asSize,
  6091. asElement = util.option.asElement,
  6092. options = this.options,
  6093. frame = this.dom.frame,
  6094. labels = this.dom.labels,
  6095. labelSet = this.dom.labelSet;
  6096. // create frame
  6097. if (!this.parent) {
  6098. throw new Error('Cannot repaint groupset: no parent attached');
  6099. }
  6100. var parentContainer = this.parent.getContainer();
  6101. if (!parentContainer) {
  6102. throw new Error('Cannot repaint groupset: parent has no container element');
  6103. }
  6104. if (!frame) {
  6105. frame = document.createElement('div');
  6106. frame.className = 'groupset';
  6107. this.dom.frame = frame;
  6108. var className = options.className;
  6109. if (className) {
  6110. util.addClassName(frame, util.option.asString(className));
  6111. }
  6112. changed += 1;
  6113. }
  6114. if (!frame.parentNode) {
  6115. parentContainer.appendChild(frame);
  6116. changed += 1;
  6117. }
  6118. // create labels
  6119. var labelContainer = asElement(options.labelContainer);
  6120. if (!labelContainer) {
  6121. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6122. }
  6123. if (!labels) {
  6124. labels = document.createElement('div');
  6125. labels.className = 'labels';
  6126. this.dom.labels = labels;
  6127. }
  6128. if (!labelSet) {
  6129. labelSet = document.createElement('div');
  6130. labelSet.className = 'label-set';
  6131. labels.appendChild(labelSet);
  6132. this.dom.labelSet = labelSet;
  6133. }
  6134. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6135. if (labels.parentNode) {
  6136. labels.parentNode.removeChild(labels.parentNode);
  6137. }
  6138. labelContainer.appendChild(labels);
  6139. }
  6140. // reposition frame
  6141. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6142. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6143. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6144. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6145. // reposition labels
  6146. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6147. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6148. var me = this,
  6149. queue = this.queue,
  6150. groups = this.groups,
  6151. groupsData = this.groupsData;
  6152. // show/hide added/changed/removed groups
  6153. var ids = Object.keys(queue);
  6154. if (ids.length) {
  6155. ids.forEach(function (id) {
  6156. var action = queue[id];
  6157. var group = groups[id];
  6158. //noinspection FallthroughInSwitchStatementJS
  6159. switch (action) {
  6160. case 'add':
  6161. case 'update':
  6162. if (!group) {
  6163. var groupOptions = Object.create(me.options);
  6164. util.extend(groupOptions, {
  6165. height: null,
  6166. maxHeight: null
  6167. });
  6168. group = new Group(me, id, groupOptions);
  6169. group.setItems(me.itemsData); // attach items data
  6170. groups[id] = group;
  6171. me.controller.add(group);
  6172. }
  6173. // TODO: update group data
  6174. group.data = groupsData.get(id);
  6175. delete queue[id];
  6176. break;
  6177. case 'remove':
  6178. if (group) {
  6179. group.setItems(); // detach items data
  6180. delete groups[id];
  6181. me.controller.remove(group);
  6182. }
  6183. // update lists
  6184. delete queue[id];
  6185. break;
  6186. default:
  6187. console.log('Error: unknown action "' + action + '"');
  6188. }
  6189. });
  6190. // the groupset depends on each of the groups
  6191. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6192. // TODO: apply dependencies of the groupset
  6193. // update the top positions of the groups in the correct order
  6194. var orderedGroups = this.groupsData.getIds({
  6195. order: this.options.groupOrder
  6196. });
  6197. for (i = 0; i < orderedGroups.length; i++) {
  6198. (function (group, prevGroup) {
  6199. var top = 0;
  6200. if (prevGroup) {
  6201. top = function () {
  6202. // TODO: top must reckon with options.maxHeight
  6203. return prevGroup.top + prevGroup.height;
  6204. }
  6205. }
  6206. group.setOptions({
  6207. top: top
  6208. });
  6209. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6210. }
  6211. // (re)create the labels
  6212. while (labelSet.firstChild) {
  6213. labelSet.removeChild(labelSet.firstChild);
  6214. }
  6215. for (i = 0; i < orderedGroups.length; i++) {
  6216. id = orderedGroups[i];
  6217. label = this._createLabel(id);
  6218. labelSet.appendChild(label);
  6219. }
  6220. changed++;
  6221. }
  6222. // reposition the labels
  6223. // TODO: labels are not displayed correctly when orientation=='top'
  6224. // TODO: width of labelPanel is not immediately updated on a change in groups
  6225. for (id in groups) {
  6226. if (groups.hasOwnProperty(id)) {
  6227. group = groups[id];
  6228. label = group.label;
  6229. if (label) {
  6230. label.style.top = group.top + 'px';
  6231. label.style.height = group.height + 'px';
  6232. }
  6233. }
  6234. }
  6235. return (changed > 0);
  6236. };
  6237. /**
  6238. * Create a label for group with given id
  6239. * @param {Number} id
  6240. * @return {Element} label
  6241. * @private
  6242. */
  6243. GroupSet.prototype._createLabel = function(id) {
  6244. var group = this.groups[id];
  6245. var label = document.createElement('div');
  6246. label.className = 'label';
  6247. var inner = document.createElement('div');
  6248. inner.className = 'inner';
  6249. label.appendChild(inner);
  6250. var content = group.data && group.data.content;
  6251. if (content instanceof Element) {
  6252. inner.appendChild(content);
  6253. }
  6254. else if (content != undefined) {
  6255. inner.innerHTML = content;
  6256. }
  6257. var className = group.data && group.data.className;
  6258. if (className) {
  6259. util.addClassName(label, className);
  6260. }
  6261. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6262. return label;
  6263. };
  6264. /**
  6265. * Get container element
  6266. * @return {HTMLElement} container
  6267. */
  6268. GroupSet.prototype.getContainer = function getContainer() {
  6269. return this.dom.frame;
  6270. };
  6271. /**
  6272. * Get the width of the group labels
  6273. * @return {Number} width
  6274. */
  6275. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6276. return this.props.labels.width;
  6277. };
  6278. /**
  6279. * Reflow the component
  6280. * @return {Boolean} resized
  6281. */
  6282. GroupSet.prototype.reflow = function reflow() {
  6283. var changed = 0,
  6284. id, group,
  6285. options = this.options,
  6286. update = util.updateProperty,
  6287. asNumber = util.option.asNumber,
  6288. asSize = util.option.asSize,
  6289. frame = this.dom.frame;
  6290. if (frame) {
  6291. var maxHeight = asNumber(options.maxHeight);
  6292. var fixedHeight = (asSize(options.height) != null);
  6293. var height;
  6294. if (fixedHeight) {
  6295. height = frame.offsetHeight;
  6296. }
  6297. else {
  6298. // height is not specified, calculate the sum of the height of all groups
  6299. height = 0;
  6300. for (id in this.groups) {
  6301. if (this.groups.hasOwnProperty(id)) {
  6302. group = this.groups[id];
  6303. height += group.height;
  6304. }
  6305. }
  6306. }
  6307. if (maxHeight != null) {
  6308. height = Math.min(height, maxHeight);
  6309. }
  6310. changed += update(this, 'height', height);
  6311. changed += update(this, 'top', frame.offsetTop);
  6312. changed += update(this, 'left', frame.offsetLeft);
  6313. changed += update(this, 'width', frame.offsetWidth);
  6314. }
  6315. // calculate the maximum width of the labels
  6316. var width = 0;
  6317. for (id in this.groups) {
  6318. if (this.groups.hasOwnProperty(id)) {
  6319. group = this.groups[id];
  6320. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  6321. width = Math.max(width, labelWidth);
  6322. }
  6323. }
  6324. changed += update(this.props.labels, 'width', width);
  6325. return (changed > 0);
  6326. };
  6327. /**
  6328. * Hide the component from the DOM
  6329. * @return {Boolean} changed
  6330. */
  6331. GroupSet.prototype.hide = function hide() {
  6332. if (this.dom.frame && this.dom.frame.parentNode) {
  6333. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6334. return true;
  6335. }
  6336. else {
  6337. return false;
  6338. }
  6339. };
  6340. /**
  6341. * Show the component in the DOM (when not already visible).
  6342. * A repaint will be executed when the component is not visible
  6343. * @return {Boolean} changed
  6344. */
  6345. GroupSet.prototype.show = function show() {
  6346. if (!this.dom.frame || !this.dom.frame.parentNode) {
  6347. return this.repaint();
  6348. }
  6349. else {
  6350. return false;
  6351. }
  6352. };
  6353. /**
  6354. * Handle updated groups
  6355. * @param {Number[]} ids
  6356. * @private
  6357. */
  6358. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6359. this._toQueue(ids, 'update');
  6360. };
  6361. /**
  6362. * Handle changed groups
  6363. * @param {Number[]} ids
  6364. * @private
  6365. */
  6366. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6367. this._toQueue(ids, 'add');
  6368. };
  6369. /**
  6370. * Handle removed groups
  6371. * @param {Number[]} ids
  6372. * @private
  6373. */
  6374. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6375. this._toQueue(ids, 'remove');
  6376. };
  6377. /**
  6378. * Put groups in the queue to be added/updated/remove
  6379. * @param {Number[]} ids
  6380. * @param {String} action can be 'add', 'update', 'remove'
  6381. */
  6382. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  6383. var queue = this.queue;
  6384. ids.forEach(function (id) {
  6385. queue[id] = action;
  6386. });
  6387. if (this.controller) {
  6388. //this.requestReflow();
  6389. this.requestRepaint();
  6390. }
  6391. };
  6392. /**
  6393. * Create a timeline visualization
  6394. * @param {HTMLElement} container
  6395. * @param {vis.DataSet | Array | DataTable} [items]
  6396. * @param {Object} [options] See Timeline.setOptions for the available options.
  6397. * @constructor
  6398. */
  6399. function Timeline (container, items, options) {
  6400. var me = this;
  6401. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6402. this.options = {
  6403. orientation: 'bottom',
  6404. min: null,
  6405. max: null,
  6406. zoomMin: 10, // milliseconds
  6407. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6408. // moveable: true, // TODO: option moveable
  6409. // zoomable: true, // TODO: option zoomable
  6410. showMinorLabels: true,
  6411. showMajorLabels: true,
  6412. showCurrentTime: false,
  6413. showCustomTime: false,
  6414. autoResize: false
  6415. };
  6416. // controller
  6417. this.controller = new Controller();
  6418. // root panel
  6419. if (!container) {
  6420. throw new Error('No container element provided');
  6421. }
  6422. var rootOptions = Object.create(this.options);
  6423. rootOptions.height = function () {
  6424. // TODO: change to height
  6425. if (me.options.height) {
  6426. // fixed height
  6427. return me.options.height;
  6428. }
  6429. else {
  6430. // auto height
  6431. return (me.timeaxis.height + me.content.height) + 'px';
  6432. }
  6433. };
  6434. this.rootPanel = new RootPanel(container, rootOptions);
  6435. this.controller.add(this.rootPanel);
  6436. // item panel
  6437. var itemOptions = Object.create(this.options);
  6438. itemOptions.left = function () {
  6439. return me.labelPanel.width;
  6440. };
  6441. itemOptions.width = function () {
  6442. return me.rootPanel.width - me.labelPanel.width;
  6443. };
  6444. itemOptions.top = null;
  6445. itemOptions.height = null;
  6446. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6447. this.controller.add(this.itemPanel);
  6448. // label panel
  6449. var labelOptions = Object.create(this.options);
  6450. labelOptions.top = null;
  6451. labelOptions.left = null;
  6452. labelOptions.height = null;
  6453. labelOptions.width = function () {
  6454. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6455. return me.content.getLabelsWidth();
  6456. }
  6457. else {
  6458. return 0;
  6459. }
  6460. };
  6461. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6462. this.controller.add(this.labelPanel);
  6463. // range
  6464. var rangeOptions = Object.create(this.options);
  6465. this.range = new Range(rangeOptions);
  6466. this.range.setRange(
  6467. now.clone().add('days', -3).valueOf(),
  6468. now.clone().add('days', 4).valueOf()
  6469. );
  6470. // TODO: reckon with options moveable and zoomable
  6471. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  6472. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  6473. this.range.on('rangechange', function () {
  6474. var force = true;
  6475. me.controller.requestReflow(force);
  6476. });
  6477. this.range.on('rangechanged', function () {
  6478. var force = true;
  6479. me.controller.requestReflow(force);
  6480. });
  6481. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  6482. // time axis
  6483. var timeaxisOptions = Object.create(rootOptions);
  6484. timeaxisOptions.range = this.range;
  6485. timeaxisOptions.left = null;
  6486. timeaxisOptions.top = null;
  6487. timeaxisOptions.width = '100%';
  6488. timeaxisOptions.height = null;
  6489. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6490. this.timeaxis.setRange(this.range);
  6491. this.controller.add(this.timeaxis);
  6492. // current time bar
  6493. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  6494. this.controller.add(this.currenttime);
  6495. // custom time bar
  6496. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  6497. this.controller.add(this.customtime);
  6498. // create groupset
  6499. this.setGroups(null);
  6500. this.itemsData = null; // DataSet
  6501. this.groupsData = null; // DataSet
  6502. // apply options
  6503. if (options) {
  6504. this.setOptions(options);
  6505. }
  6506. // create itemset and groupset
  6507. if (items) {
  6508. this.setItems(items);
  6509. }
  6510. }
  6511. /**
  6512. * Set options
  6513. * @param {Object} options TODO: describe the available options
  6514. */
  6515. Timeline.prototype.setOptions = function (options) {
  6516. util.extend(this.options, options);
  6517. // force update of range
  6518. // options.start and options.end can be undefined
  6519. //this.range.setRange(options.start, options.end);
  6520. this.range.setRange();
  6521. this.controller.reflow();
  6522. this.controller.repaint();
  6523. };
  6524. /**
  6525. * Set a custom time bar
  6526. * @param {Date} time
  6527. */
  6528. Timeline.prototype.setCustomTime = function (time) {
  6529. this.customtime._setCustomTime(time);
  6530. };
  6531. /**
  6532. * Retrieve the current custom time.
  6533. * @return {Date} customTime
  6534. */
  6535. Timeline.prototype.getCustomTime = function() {
  6536. return new Date(this.customtime.customTime.valueOf());
  6537. };
  6538. /**
  6539. * Set items
  6540. * @param {vis.DataSet | Array | DataTable | null} items
  6541. */
  6542. Timeline.prototype.setItems = function(items) {
  6543. var initialLoad = (this.itemsData == null);
  6544. // convert to type DataSet when needed
  6545. var newItemSet;
  6546. if (!items) {
  6547. newItemSet = null;
  6548. }
  6549. else if (items instanceof DataSet) {
  6550. newItemSet = items;
  6551. }
  6552. if (!(items instanceof DataSet)) {
  6553. newItemSet = new DataSet({
  6554. convert: {
  6555. start: 'Date',
  6556. end: 'Date'
  6557. }
  6558. });
  6559. newItemSet.add(items);
  6560. }
  6561. // set items
  6562. this.itemsData = newItemSet;
  6563. this.content.setItems(newItemSet);
  6564. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6565. // apply the data range as range
  6566. var dataRange = this.getItemRange();
  6567. // add 5% space on both sides
  6568. var min = dataRange.min;
  6569. var max = dataRange.max;
  6570. if (min != null && max != null) {
  6571. var interval = (max.valueOf() - min.valueOf());
  6572. if (interval <= 0) {
  6573. // prevent an empty interval
  6574. interval = 24 * 60 * 60 * 1000; // 1 day
  6575. }
  6576. min = new Date(min.valueOf() - interval * 0.05);
  6577. max = new Date(max.valueOf() + interval * 0.05);
  6578. }
  6579. // override specified start and/or end date
  6580. if (this.options.start != undefined) {
  6581. min = util.convert(this.options.start, 'Date');
  6582. }
  6583. if (this.options.end != undefined) {
  6584. max = util.convert(this.options.end, 'Date');
  6585. }
  6586. // apply range if there is a min or max available
  6587. if (min != null || max != null) {
  6588. this.range.setRange(min, max);
  6589. }
  6590. }
  6591. };
  6592. /**
  6593. * Set groups
  6594. * @param {vis.DataSet | Array | DataTable} groups
  6595. */
  6596. Timeline.prototype.setGroups = function(groups) {
  6597. var me = this;
  6598. this.groupsData = groups;
  6599. // switch content type between ItemSet or GroupSet when needed
  6600. var Type = this.groupsData ? GroupSet : ItemSet;
  6601. if (!(this.content instanceof Type)) {
  6602. // remove old content set
  6603. if (this.content) {
  6604. this.content.hide();
  6605. if (this.content.setItems) {
  6606. this.content.setItems(); // disconnect from items
  6607. }
  6608. if (this.content.setGroups) {
  6609. this.content.setGroups(); // disconnect from groups
  6610. }
  6611. this.controller.remove(this.content);
  6612. }
  6613. // create new content set
  6614. var options = Object.create(this.options);
  6615. util.extend(options, {
  6616. top: function () {
  6617. if (me.options.orientation == 'top') {
  6618. return me.timeaxis.height;
  6619. }
  6620. else {
  6621. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  6622. }
  6623. },
  6624. left: null,
  6625. width: '100%',
  6626. height: function () {
  6627. if (me.options.height) {
  6628. // fixed height
  6629. return me.itemPanel.height - me.timeaxis.height;
  6630. }
  6631. else {
  6632. // auto height
  6633. return null;
  6634. }
  6635. },
  6636. maxHeight: function () {
  6637. // TODO: change maxHeight to be a css string like '100%' or '300px'
  6638. if (me.options.maxHeight) {
  6639. if (!util.isNumber(me.options.maxHeight)) {
  6640. throw new TypeError('Number expected for property maxHeight');
  6641. }
  6642. return me.options.maxHeight - me.timeaxis.height;
  6643. }
  6644. else {
  6645. return null;
  6646. }
  6647. },
  6648. labelContainer: function () {
  6649. return me.labelPanel.getContainer();
  6650. }
  6651. });
  6652. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  6653. if (this.content.setRange) {
  6654. this.content.setRange(this.range);
  6655. }
  6656. if (this.content.setItems) {
  6657. this.content.setItems(this.itemsData);
  6658. }
  6659. if (this.content.setGroups) {
  6660. this.content.setGroups(this.groupsData);
  6661. }
  6662. this.controller.add(this.content);
  6663. }
  6664. };
  6665. /**
  6666. * Get the data range of the item set.
  6667. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6668. * When no minimum is found, min==null
  6669. * When no maximum is found, max==null
  6670. */
  6671. Timeline.prototype.getItemRange = function getItemRange() {
  6672. // calculate min from start filed
  6673. var itemsData = this.itemsData,
  6674. min = null,
  6675. max = null;
  6676. if (itemsData) {
  6677. // calculate the minimum value of the field 'start'
  6678. var minItem = itemsData.min('start');
  6679. min = minItem ? minItem.start.valueOf() : null;
  6680. // calculate maximum value of fields 'start' and 'end'
  6681. var maxStartItem = itemsData.max('start');
  6682. if (maxStartItem) {
  6683. max = maxStartItem.start.valueOf();
  6684. }
  6685. var maxEndItem = itemsData.max('end');
  6686. if (maxEndItem) {
  6687. if (max == null) {
  6688. max = maxEndItem.end.valueOf();
  6689. }
  6690. else {
  6691. max = Math.max(max, maxEndItem.end.valueOf());
  6692. }
  6693. }
  6694. }
  6695. return {
  6696. min: (min != null) ? new Date(min) : null,
  6697. max: (max != null) ? new Date(max) : null
  6698. };
  6699. };
  6700. (function(exports) {
  6701. /**
  6702. * Parse a text source containing data in DOT language into a JSON object.
  6703. * The object contains two lists: one with nodes and one with edges.
  6704. *
  6705. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6706. *
  6707. * @param {String} data Text containing a graph in DOT-notation
  6708. * @return {Object} graph An object containing two parameters:
  6709. * {Object[]} nodes
  6710. * {Object[]} edges
  6711. */
  6712. function parseDOT (data) {
  6713. dot = data;
  6714. return parseGraph();
  6715. }
  6716. // token types enumeration
  6717. var TOKENTYPE = {
  6718. NULL : 0,
  6719. DELIMITER : 1,
  6720. IDENTIFIER: 2,
  6721. UNKNOWN : 3
  6722. };
  6723. // map with all delimiters
  6724. var DELIMITERS = {
  6725. '{': true,
  6726. '}': true,
  6727. '[': true,
  6728. ']': true,
  6729. ';': true,
  6730. '=': true,
  6731. ',': true,
  6732. '->': true,
  6733. '--': true
  6734. };
  6735. var dot = ''; // current dot file
  6736. var index = 0; // current index in dot file
  6737. var c = ''; // current token character in expr
  6738. var token = ''; // current token
  6739. var tokenType = TOKENTYPE.NULL; // type of the token
  6740. /**
  6741. * Get the first character from the dot file.
  6742. * The character is stored into the char c. If the end of the dot file is
  6743. * reached, the function puts an empty string in c.
  6744. */
  6745. function first() {
  6746. index = 0;
  6747. c = dot.charAt(0);
  6748. }
  6749. /**
  6750. * Get the next character from the dot file.
  6751. * The character is stored into the char c. If the end of the dot file is
  6752. * reached, the function puts an empty string in c.
  6753. */
  6754. function next() {
  6755. index++;
  6756. c = dot.charAt(index);
  6757. }
  6758. /**
  6759. * Preview the next character from the dot file.
  6760. * @return {String} cNext
  6761. */
  6762. function nextPreview() {
  6763. return dot.charAt(index + 1);
  6764. }
  6765. /**
  6766. * Test whether given character is alphabetic or numeric
  6767. * @param {String} c
  6768. * @return {Boolean} isAlphaNumeric
  6769. */
  6770. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6771. function isAlphaNumeric(c) {
  6772. return regexAlphaNumeric.test(c);
  6773. }
  6774. /**
  6775. * Merge all properties of object b into object b
  6776. * @param {Object} a
  6777. * @param {Object} b
  6778. * @return {Object} a
  6779. */
  6780. function merge (a, b) {
  6781. if (!a) {
  6782. a = {};
  6783. }
  6784. if (b) {
  6785. for (var name in b) {
  6786. if (b.hasOwnProperty(name)) {
  6787. a[name] = b[name];
  6788. }
  6789. }
  6790. }
  6791. return a;
  6792. }
  6793. /**
  6794. * Set a value in an object, where the provided parameter name can be a
  6795. * path with nested parameters. For example:
  6796. *
  6797. * var obj = {a: 2};
  6798. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6799. *
  6800. * @param {Object} obj
  6801. * @param {String} path A parameter name or dot-separated parameter path,
  6802. * like "color.highlight.border".
  6803. * @param {*} value
  6804. */
  6805. function setValue(obj, path, value) {
  6806. var keys = path.split('.');
  6807. var o = obj;
  6808. while (keys.length) {
  6809. var key = keys.shift();
  6810. if (keys.length) {
  6811. // this isn't the end point
  6812. if (!o[key]) {
  6813. o[key] = {};
  6814. }
  6815. o = o[key];
  6816. }
  6817. else {
  6818. // this is the end point
  6819. o[key] = value;
  6820. }
  6821. }
  6822. }
  6823. /**
  6824. * Add a node to a graph object. If there is already a node with
  6825. * the same id, their attributes will be merged.
  6826. * @param {Object} graph
  6827. * @param {Object} node
  6828. */
  6829. function addNode(graph, node) {
  6830. var i, len;
  6831. var current = null;
  6832. // find root graph (in case of subgraph)
  6833. var graphs = [graph]; // list with all graphs from current graph to root graph
  6834. var root = graph;
  6835. while (root.parent) {
  6836. graphs.push(root.parent);
  6837. root = root.parent;
  6838. }
  6839. // find existing node (at root level) by its id
  6840. if (root.nodes) {
  6841. for (i = 0, len = root.nodes.length; i < len; i++) {
  6842. if (node.id === root.nodes[i].id) {
  6843. current = root.nodes[i];
  6844. break;
  6845. }
  6846. }
  6847. }
  6848. if (!current) {
  6849. // this is a new node
  6850. current = {
  6851. id: node.id
  6852. };
  6853. if (graph.node) {
  6854. // clone default attributes
  6855. current.attr = merge(current.attr, graph.node);
  6856. }
  6857. }
  6858. // add node to this (sub)graph and all its parent graphs
  6859. for (i = graphs.length - 1; i >= 0; i--) {
  6860. var g = graphs[i];
  6861. if (!g.nodes) {
  6862. g.nodes = [];
  6863. }
  6864. if (g.nodes.indexOf(current) == -1) {
  6865. g.nodes.push(current);
  6866. }
  6867. }
  6868. // merge attributes
  6869. if (node.attr) {
  6870. current.attr = merge(current.attr, node.attr);
  6871. }
  6872. }
  6873. /**
  6874. * Add an edge to a graph object
  6875. * @param {Object} graph
  6876. * @param {Object} edge
  6877. */
  6878. function addEdge(graph, edge) {
  6879. if (!graph.edges) {
  6880. graph.edges = [];
  6881. }
  6882. graph.edges.push(edge);
  6883. if (graph.edge) {
  6884. var attr = merge({}, graph.edge); // clone default attributes
  6885. edge.attr = merge(attr, edge.attr); // merge attributes
  6886. }
  6887. }
  6888. /**
  6889. * Create an edge to a graph object
  6890. * @param {Object} graph
  6891. * @param {String | Number | Object} from
  6892. * @param {String | Number | Object} to
  6893. * @param {String} type
  6894. * @param {Object | null} attr
  6895. * @return {Object} edge
  6896. */
  6897. function createEdge(graph, from, to, type, attr) {
  6898. var edge = {
  6899. from: from,
  6900. to: to,
  6901. type: type
  6902. };
  6903. if (graph.edge) {
  6904. edge.attr = merge({}, graph.edge); // clone default attributes
  6905. }
  6906. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6907. return edge;
  6908. }
  6909. /**
  6910. * Get next token in the current dot file.
  6911. * The token and token type are available as token and tokenType
  6912. */
  6913. function getToken() {
  6914. tokenType = TOKENTYPE.NULL;
  6915. token = '';
  6916. // skip over whitespaces
  6917. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6918. next();
  6919. }
  6920. do {
  6921. var isComment = false;
  6922. // skip comment
  6923. if (c == '#') {
  6924. // find the previous non-space character
  6925. var i = index - 1;
  6926. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  6927. i--;
  6928. }
  6929. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  6930. // the # is at the start of a line, this is indeed a line comment
  6931. while (c != '' && c != '\n') {
  6932. next();
  6933. }
  6934. isComment = true;
  6935. }
  6936. }
  6937. if (c == '/' && nextPreview() == '/') {
  6938. // skip line comment
  6939. while (c != '' && c != '\n') {
  6940. next();
  6941. }
  6942. isComment = true;
  6943. }
  6944. if (c == '/' && nextPreview() == '*') {
  6945. // skip block comment
  6946. while (c != '') {
  6947. if (c == '*' && nextPreview() == '/') {
  6948. // end of block comment found. skip these last two characters
  6949. next();
  6950. next();
  6951. break;
  6952. }
  6953. else {
  6954. next();
  6955. }
  6956. }
  6957. isComment = true;
  6958. }
  6959. // skip over whitespaces
  6960. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6961. next();
  6962. }
  6963. }
  6964. while (isComment);
  6965. // check for end of dot file
  6966. if (c == '') {
  6967. // token is still empty
  6968. tokenType = TOKENTYPE.DELIMITER;
  6969. return;
  6970. }
  6971. // check for delimiters consisting of 2 characters
  6972. var c2 = c + nextPreview();
  6973. if (DELIMITERS[c2]) {
  6974. tokenType = TOKENTYPE.DELIMITER;
  6975. token = c2;
  6976. next();
  6977. next();
  6978. return;
  6979. }
  6980. // check for delimiters consisting of 1 character
  6981. if (DELIMITERS[c]) {
  6982. tokenType = TOKENTYPE.DELIMITER;
  6983. token = c;
  6984. next();
  6985. return;
  6986. }
  6987. // check for an identifier (number or string)
  6988. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  6989. if (isAlphaNumeric(c) || c == '-') {
  6990. token += c;
  6991. next();
  6992. while (isAlphaNumeric(c)) {
  6993. token += c;
  6994. next();
  6995. }
  6996. if (token == 'false') {
  6997. token = false; // convert to boolean
  6998. }
  6999. else if (token == 'true') {
  7000. token = true; // convert to boolean
  7001. }
  7002. else if (!isNaN(Number(token))) {
  7003. token = Number(token); // convert to number
  7004. }
  7005. tokenType = TOKENTYPE.IDENTIFIER;
  7006. return;
  7007. }
  7008. // check for a string enclosed by double quotes
  7009. if (c == '"') {
  7010. next();
  7011. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7012. token += c;
  7013. if (c == '"') { // skip the escape character
  7014. next();
  7015. }
  7016. next();
  7017. }
  7018. if (c != '"') {
  7019. throw newSyntaxError('End of string " expected');
  7020. }
  7021. next();
  7022. tokenType = TOKENTYPE.IDENTIFIER;
  7023. return;
  7024. }
  7025. // something unknown is found, wrong characters, a syntax error
  7026. tokenType = TOKENTYPE.UNKNOWN;
  7027. while (c != '') {
  7028. token += c;
  7029. next();
  7030. }
  7031. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7032. }
  7033. /**
  7034. * Parse a graph.
  7035. * @returns {Object} graph
  7036. */
  7037. function parseGraph() {
  7038. var graph = {};
  7039. first();
  7040. getToken();
  7041. // optional strict keyword
  7042. if (token == 'strict') {
  7043. graph.strict = true;
  7044. getToken();
  7045. }
  7046. // graph or digraph keyword
  7047. if (token == 'graph' || token == 'digraph') {
  7048. graph.type = token;
  7049. getToken();
  7050. }
  7051. // optional graph id
  7052. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7053. graph.id = token;
  7054. getToken();
  7055. }
  7056. // open angle bracket
  7057. if (token != '{') {
  7058. throw newSyntaxError('Angle bracket { expected');
  7059. }
  7060. getToken();
  7061. // statements
  7062. parseStatements(graph);
  7063. // close angle bracket
  7064. if (token != '}') {
  7065. throw newSyntaxError('Angle bracket } expected');
  7066. }
  7067. getToken();
  7068. // end of file
  7069. if (token !== '') {
  7070. throw newSyntaxError('End of file expected');
  7071. }
  7072. getToken();
  7073. // remove temporary default properties
  7074. delete graph.node;
  7075. delete graph.edge;
  7076. delete graph.graph;
  7077. return graph;
  7078. }
  7079. /**
  7080. * Parse a list with statements.
  7081. * @param {Object} graph
  7082. */
  7083. function parseStatements (graph) {
  7084. while (token !== '' && token != '}') {
  7085. parseStatement(graph);
  7086. if (token == ';') {
  7087. getToken();
  7088. }
  7089. }
  7090. }
  7091. /**
  7092. * Parse a single statement. Can be a an attribute statement, node
  7093. * statement, a series of node statements and edge statements, or a
  7094. * parameter.
  7095. * @param {Object} graph
  7096. */
  7097. function parseStatement(graph) {
  7098. // parse subgraph
  7099. var subgraph = parseSubgraph(graph);
  7100. if (subgraph) {
  7101. // edge statements
  7102. parseEdge(graph, subgraph);
  7103. return;
  7104. }
  7105. // parse an attribute statement
  7106. var attr = parseAttributeStatement(graph);
  7107. if (attr) {
  7108. return;
  7109. }
  7110. // parse node
  7111. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7112. throw newSyntaxError('Identifier expected');
  7113. }
  7114. var id = token; // id can be a string or a number
  7115. getToken();
  7116. if (token == '=') {
  7117. // id statement
  7118. getToken();
  7119. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7120. throw newSyntaxError('Identifier expected');
  7121. }
  7122. graph[id] = token;
  7123. getToken();
  7124. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7125. }
  7126. else {
  7127. parseNodeStatement(graph, id);
  7128. }
  7129. }
  7130. /**
  7131. * Parse a subgraph
  7132. * @param {Object} graph parent graph object
  7133. * @return {Object | null} subgraph
  7134. */
  7135. function parseSubgraph (graph) {
  7136. var subgraph = null;
  7137. // optional subgraph keyword
  7138. if (token == 'subgraph') {
  7139. subgraph = {};
  7140. subgraph.type = 'subgraph';
  7141. getToken();
  7142. // optional graph id
  7143. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7144. subgraph.id = token;
  7145. getToken();
  7146. }
  7147. }
  7148. // open angle bracket
  7149. if (token == '{') {
  7150. getToken();
  7151. if (!subgraph) {
  7152. subgraph = {};
  7153. }
  7154. subgraph.parent = graph;
  7155. subgraph.node = graph.node;
  7156. subgraph.edge = graph.edge;
  7157. subgraph.graph = graph.graph;
  7158. // statements
  7159. parseStatements(subgraph);
  7160. // close angle bracket
  7161. if (token != '}') {
  7162. throw newSyntaxError('Angle bracket } expected');
  7163. }
  7164. getToken();
  7165. // remove temporary default properties
  7166. delete subgraph.node;
  7167. delete subgraph.edge;
  7168. delete subgraph.graph;
  7169. delete subgraph.parent;
  7170. // register at the parent graph
  7171. if (!graph.subgraphs) {
  7172. graph.subgraphs = [];
  7173. }
  7174. graph.subgraphs.push(subgraph);
  7175. }
  7176. return subgraph;
  7177. }
  7178. /**
  7179. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7180. * Available keywords are 'node', 'edge', 'graph'.
  7181. * The previous list with default attributes will be replaced
  7182. * @param {Object} graph
  7183. * @returns {String | null} keyword Returns the name of the parsed attribute
  7184. * (node, edge, graph), or null if nothing
  7185. * is parsed.
  7186. */
  7187. function parseAttributeStatement (graph) {
  7188. // attribute statements
  7189. if (token == 'node') {
  7190. getToken();
  7191. // node attributes
  7192. graph.node = parseAttributeList();
  7193. return 'node';
  7194. }
  7195. else if (token == 'edge') {
  7196. getToken();
  7197. // edge attributes
  7198. graph.edge = parseAttributeList();
  7199. return 'edge';
  7200. }
  7201. else if (token == 'graph') {
  7202. getToken();
  7203. // graph attributes
  7204. graph.graph = parseAttributeList();
  7205. return 'graph';
  7206. }
  7207. return null;
  7208. }
  7209. /**
  7210. * parse a node statement
  7211. * @param {Object} graph
  7212. * @param {String | Number} id
  7213. */
  7214. function parseNodeStatement(graph, id) {
  7215. // node statement
  7216. var node = {
  7217. id: id
  7218. };
  7219. var attr = parseAttributeList();
  7220. if (attr) {
  7221. node.attr = attr;
  7222. }
  7223. addNode(graph, node);
  7224. // edge statements
  7225. parseEdge(graph, id);
  7226. }
  7227. /**
  7228. * Parse an edge or a series of edges
  7229. * @param {Object} graph
  7230. * @param {String | Number} from Id of the from node
  7231. */
  7232. function parseEdge(graph, from) {
  7233. while (token == '->' || token == '--') {
  7234. var to;
  7235. var type = token;
  7236. getToken();
  7237. var subgraph = parseSubgraph(graph);
  7238. if (subgraph) {
  7239. to = subgraph;
  7240. }
  7241. else {
  7242. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7243. throw newSyntaxError('Identifier or subgraph expected');
  7244. }
  7245. to = token;
  7246. addNode(graph, {
  7247. id: to
  7248. });
  7249. getToken();
  7250. }
  7251. // parse edge attributes
  7252. var attr = parseAttributeList();
  7253. // create edge
  7254. var edge = createEdge(graph, from, to, type, attr);
  7255. addEdge(graph, edge);
  7256. from = to;
  7257. }
  7258. }
  7259. /**
  7260. * Parse a set with attributes,
  7261. * for example [label="1.000", shape=solid]
  7262. * @return {Object | null} attr
  7263. */
  7264. function parseAttributeList() {
  7265. var attr = null;
  7266. while (token == '[') {
  7267. getToken();
  7268. attr = {};
  7269. while (token !== '' && token != ']') {
  7270. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7271. throw newSyntaxError('Attribute name expected');
  7272. }
  7273. var name = token;
  7274. getToken();
  7275. if (token != '=') {
  7276. throw newSyntaxError('Equal sign = expected');
  7277. }
  7278. getToken();
  7279. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7280. throw newSyntaxError('Attribute value expected');
  7281. }
  7282. var value = token;
  7283. setValue(attr, name, value); // name can be a path
  7284. getToken();
  7285. if (token ==',') {
  7286. getToken();
  7287. }
  7288. }
  7289. if (token != ']') {
  7290. throw newSyntaxError('Bracket ] expected');
  7291. }
  7292. getToken();
  7293. }
  7294. return attr;
  7295. }
  7296. /**
  7297. * Create a syntax error with extra information on current token and index.
  7298. * @param {String} message
  7299. * @returns {SyntaxError} err
  7300. */
  7301. function newSyntaxError(message) {
  7302. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7303. }
  7304. /**
  7305. * Chop off text after a maximum length
  7306. * @param {String} text
  7307. * @param {Number} maxLength
  7308. * @returns {String}
  7309. */
  7310. function chop (text, maxLength) {
  7311. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7312. }
  7313. /**
  7314. * Execute a function fn for each pair of elements in two arrays
  7315. * @param {Array | *} array1
  7316. * @param {Array | *} array2
  7317. * @param {function} fn
  7318. */
  7319. function forEach2(array1, array2, fn) {
  7320. if (array1 instanceof Array) {
  7321. array1.forEach(function (elem1) {
  7322. if (array2 instanceof Array) {
  7323. array2.forEach(function (elem2) {
  7324. fn(elem1, elem2);
  7325. });
  7326. }
  7327. else {
  7328. fn(elem1, array2);
  7329. }
  7330. });
  7331. }
  7332. else {
  7333. if (array2 instanceof Array) {
  7334. array2.forEach(function (elem2) {
  7335. fn(array1, elem2);
  7336. });
  7337. }
  7338. else {
  7339. fn(array1, array2);
  7340. }
  7341. }
  7342. }
  7343. /**
  7344. * Convert a string containing a graph in DOT language into a map containing
  7345. * with nodes and edges in the format of graph.
  7346. * @param {String} data Text containing a graph in DOT-notation
  7347. * @return {Object} graphData
  7348. */
  7349. function DOTToGraph (data) {
  7350. // parse the DOT file
  7351. var dotData = parseDOT(data);
  7352. var graphData = {
  7353. nodes: [],
  7354. edges: [],
  7355. options: {}
  7356. };
  7357. // copy the nodes
  7358. if (dotData.nodes) {
  7359. dotData.nodes.forEach(function (dotNode) {
  7360. var graphNode = {
  7361. id: dotNode.id,
  7362. label: String(dotNode.label || dotNode.id)
  7363. };
  7364. merge(graphNode, dotNode.attr);
  7365. if (graphNode.image) {
  7366. graphNode.shape = 'image';
  7367. }
  7368. graphData.nodes.push(graphNode);
  7369. });
  7370. }
  7371. // copy the edges
  7372. if (dotData.edges) {
  7373. /**
  7374. * Convert an edge in DOT format to an edge with VisGraph format
  7375. * @param {Object} dotEdge
  7376. * @returns {Object} graphEdge
  7377. */
  7378. function convertEdge(dotEdge) {
  7379. var graphEdge = {
  7380. from: dotEdge.from,
  7381. to: dotEdge.to
  7382. };
  7383. merge(graphEdge, dotEdge.attr);
  7384. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7385. return graphEdge;
  7386. }
  7387. dotData.edges.forEach(function (dotEdge) {
  7388. var from, to;
  7389. if (dotEdge.from instanceof Object) {
  7390. from = dotEdge.from.nodes;
  7391. }
  7392. else {
  7393. from = {
  7394. id: dotEdge.from
  7395. }
  7396. }
  7397. if (dotEdge.to instanceof Object) {
  7398. to = dotEdge.to.nodes;
  7399. }
  7400. else {
  7401. to = {
  7402. id: dotEdge.to
  7403. }
  7404. }
  7405. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7406. dotEdge.from.edges.forEach(function (subEdge) {
  7407. var graphEdge = convertEdge(subEdge);
  7408. graphData.edges.push(graphEdge);
  7409. });
  7410. }
  7411. forEach2(from, to, function (from, to) {
  7412. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7413. var graphEdge = convertEdge(subEdge);
  7414. graphData.edges.push(graphEdge);
  7415. });
  7416. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7417. dotEdge.to.edges.forEach(function (subEdge) {
  7418. var graphEdge = convertEdge(subEdge);
  7419. graphData.edges.push(graphEdge);
  7420. });
  7421. }
  7422. });
  7423. }
  7424. // copy the options
  7425. if (dotData.attr) {
  7426. graphData.options = dotData.attr;
  7427. }
  7428. return graphData;
  7429. }
  7430. // exports
  7431. exports.parseDOT = parseDOT;
  7432. exports.DOTToGraph = DOTToGraph;
  7433. })(typeof util !== 'undefined' ? util : exports);
  7434. /**
  7435. * Canvas shapes used by the Graph
  7436. */
  7437. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7438. /**
  7439. * Draw a circle shape
  7440. */
  7441. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7442. this.beginPath();
  7443. this.arc(x, y, r, 0, 2*Math.PI, false);
  7444. };
  7445. /**
  7446. * Draw a square shape
  7447. * @param {Number} x horizontal center
  7448. * @param {Number} y vertical center
  7449. * @param {Number} r size, width and height of the square
  7450. */
  7451. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7452. this.beginPath();
  7453. this.rect(x - r, y - r, r * 2, r * 2);
  7454. };
  7455. /**
  7456. * Draw a triangle shape
  7457. * @param {Number} x horizontal center
  7458. * @param {Number} y vertical center
  7459. * @param {Number} r radius, half the length of the sides of the triangle
  7460. */
  7461. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7462. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7463. this.beginPath();
  7464. var s = r * 2;
  7465. var s2 = s / 2;
  7466. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7467. var h = Math.sqrt(s * s - s2 * s2); // height
  7468. this.moveTo(x, y - (h - ir));
  7469. this.lineTo(x + s2, y + ir);
  7470. this.lineTo(x - s2, y + ir);
  7471. this.lineTo(x, y - (h - ir));
  7472. this.closePath();
  7473. };
  7474. /**
  7475. * Draw a triangle shape in downward orientation
  7476. * @param {Number} x horizontal center
  7477. * @param {Number} y vertical center
  7478. * @param {Number} r radius
  7479. */
  7480. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7481. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7482. this.beginPath();
  7483. var s = r * 2;
  7484. var s2 = s / 2;
  7485. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7486. var h = Math.sqrt(s * s - s2 * s2); // height
  7487. this.moveTo(x, y + (h - ir));
  7488. this.lineTo(x + s2, y - ir);
  7489. this.lineTo(x - s2, y - ir);
  7490. this.lineTo(x, y + (h - ir));
  7491. this.closePath();
  7492. };
  7493. /**
  7494. * Draw a star shape, a star with 5 points
  7495. * @param {Number} x horizontal center
  7496. * @param {Number} y vertical center
  7497. * @param {Number} r radius, half the length of the sides of the triangle
  7498. */
  7499. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7500. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7501. this.beginPath();
  7502. for (var n = 0; n < 10; n++) {
  7503. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7504. this.lineTo(
  7505. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7506. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7507. );
  7508. }
  7509. this.closePath();
  7510. };
  7511. /**
  7512. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7513. */
  7514. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7515. var r2d = Math.PI/180;
  7516. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7517. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7518. this.beginPath();
  7519. this.moveTo(x+r,y);
  7520. this.lineTo(x+w-r,y);
  7521. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7522. this.lineTo(x+w,y+h-r);
  7523. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7524. this.lineTo(x+r,y+h);
  7525. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7526. this.lineTo(x,y+r);
  7527. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7528. };
  7529. /**
  7530. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7531. */
  7532. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7533. var kappa = .5522848,
  7534. ox = (w / 2) * kappa, // control point offset horizontal
  7535. oy = (h / 2) * kappa, // control point offset vertical
  7536. xe = x + w, // x-end
  7537. ye = y + h, // y-end
  7538. xm = x + w / 2, // x-middle
  7539. ym = y + h / 2; // y-middle
  7540. this.beginPath();
  7541. this.moveTo(x, ym);
  7542. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7543. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7544. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7545. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7546. };
  7547. /**
  7548. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7549. */
  7550. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7551. var f = 1/3;
  7552. var wEllipse = w;
  7553. var hEllipse = h * f;
  7554. var kappa = .5522848,
  7555. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7556. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7557. xe = x + wEllipse, // x-end
  7558. ye = y + hEllipse, // y-end
  7559. xm = x + wEllipse / 2, // x-middle
  7560. ym = y + hEllipse / 2, // y-middle
  7561. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7562. yeb = y + h; // y-end, bottom ellipse
  7563. this.beginPath();
  7564. this.moveTo(xe, ym);
  7565. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7566. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7567. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7568. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7569. this.lineTo(xe, ymb);
  7570. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7571. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7572. this.lineTo(x, ym);
  7573. };
  7574. /**
  7575. * Draw an arrow point (no line)
  7576. */
  7577. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7578. // tail
  7579. var xt = x - length * Math.cos(angle);
  7580. var yt = y - length * Math.sin(angle);
  7581. // inner tail
  7582. // TODO: allow to customize different shapes
  7583. var xi = x - length * 0.9 * Math.cos(angle);
  7584. var yi = y - length * 0.9 * Math.sin(angle);
  7585. // left
  7586. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7587. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7588. // right
  7589. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7590. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7591. this.beginPath();
  7592. this.moveTo(x, y);
  7593. this.lineTo(xl, yl);
  7594. this.lineTo(xi, yi);
  7595. this.lineTo(xr, yr);
  7596. this.closePath();
  7597. };
  7598. /**
  7599. * Sets up the dashedLine functionality for drawing
  7600. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7601. * @author David Jordan
  7602. * @date 2012-08-08
  7603. */
  7604. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7605. if (!dashArray) dashArray=[10,5];
  7606. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7607. var dashCount = dashArray.length;
  7608. this.moveTo(x, y);
  7609. var dx = (x2-x), dy = (y2-y);
  7610. var slope = dy/dx;
  7611. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7612. var dashIndex=0, draw=true;
  7613. while (distRemaining>=0.1){
  7614. var dashLength = dashArray[dashIndex++%dashCount];
  7615. if (dashLength > distRemaining) dashLength = distRemaining;
  7616. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7617. if (dx<0) xStep = -xStep;
  7618. x += xStep;
  7619. y += slope*xStep;
  7620. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7621. distRemaining -= dashLength;
  7622. draw = !draw;
  7623. }
  7624. };
  7625. // TODO: add diamond shape
  7626. }
  7627. /**
  7628. * @class Node
  7629. * A node. A node can be connected to other nodes via one or multiple edges.
  7630. * @param {object} properties An object containing properties for the node. All
  7631. * properties are optional, except for the id.
  7632. * {number} id Id of the node. Required
  7633. * {string} label Text label for the node
  7634. * {number} x Horizontal position of the node
  7635. * {number} y Vertical position of the node
  7636. * {string} shape Node shape, available:
  7637. * "database", "circle", "ellipse",
  7638. * "box", "image", "text", "dot",
  7639. * "star", "triangle", "triangleDown",
  7640. * "square"
  7641. * {string} image An image url
  7642. * {string} title An title text, can be HTML
  7643. * {anytype} group A group name or number
  7644. * @param {Graph.Images} imagelist A list with images. Only needed
  7645. * when the node has an image
  7646. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7647. * retrieving group properties
  7648. * @param {Object} constants An object with default values for
  7649. * example for the color
  7650. */
  7651. function Node(properties, imagelist, grouplist, constants) {
  7652. this.selected = false;
  7653. this.edges = []; // all edges connected to this node
  7654. this.group = constants.nodes.group;
  7655. this.fontSize = constants.nodes.fontSize;
  7656. this.fontFace = constants.nodes.fontFace;
  7657. this.fontColor = constants.nodes.fontColor;
  7658. this.color = constants.nodes.color;
  7659. // set defaults for the properties
  7660. this.id = undefined;
  7661. this.shape = constants.nodes.shape;
  7662. this.image = constants.nodes.image;
  7663. this.x = 0;
  7664. this.y = 0;
  7665. this.xFixed = false;
  7666. this.yFixed = false;
  7667. this.radius = constants.nodes.radius;
  7668. this.radiusFixed = false;
  7669. this.radiusMin = constants.nodes.radiusMin;
  7670. this.radiusMax = constants.nodes.radiusMax;
  7671. this.imagelist = imagelist;
  7672. this.grouplist = grouplist;
  7673. this.setProperties(properties, constants);
  7674. // mass, force, velocity
  7675. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  7676. this.fx = 0.0; // external force x
  7677. this.fy = 0.0; // external force y
  7678. this.vx = 0.0; // velocity x
  7679. this.vy = 0.0; // velocity y
  7680. this.minForce = constants.minForce;
  7681. this.damping = 0.9; // damping factor
  7682. };
  7683. /**
  7684. * Attach a edge to the node
  7685. * @param {Edge} edge
  7686. */
  7687. Node.prototype.attachEdge = function(edge) {
  7688. if (this.edges.indexOf(edge) == -1) {
  7689. this.edges.push(edge);
  7690. }
  7691. this._updateMass();
  7692. };
  7693. /**
  7694. * Detach a edge from the node
  7695. * @param {Edge} edge
  7696. */
  7697. Node.prototype.detachEdge = function(edge) {
  7698. var index = this.edges.indexOf(edge);
  7699. if (index != -1) {
  7700. this.edges.splice(index, 1);
  7701. }
  7702. this._updateMass();
  7703. };
  7704. /**
  7705. * Update the nodes mass, which is determined by the number of edges connecting
  7706. * to it (more edges -> heavier node).
  7707. * @private
  7708. */
  7709. Node.prototype._updateMass = function() {
  7710. this.mass = 50 + 20 * this.edges.length; // kg
  7711. };
  7712. /**
  7713. * Set or overwrite properties for the node
  7714. * @param {Object} properties an object with properties
  7715. * @param {Object} constants and object with default, global properties
  7716. */
  7717. Node.prototype.setProperties = function(properties, constants) {
  7718. if (!properties) {
  7719. return;
  7720. }
  7721. // basic properties
  7722. if (properties.id != undefined) {this.id = properties.id;}
  7723. if (properties.label != undefined) {this.label = properties.label;}
  7724. if (properties.title != undefined) {this.title = properties.title;}
  7725. if (properties.group != undefined) {this.group = properties.group;}
  7726. if (properties.x != undefined) {this.x = properties.x;}
  7727. if (properties.y != undefined) {this.y = properties.y;}
  7728. if (properties.value != undefined) {this.value = properties.value;}
  7729. if (this.id === undefined) {
  7730. throw "Node must have an id";
  7731. }
  7732. // copy group properties
  7733. if (this.group) {
  7734. var groupObj = this.grouplist.get(this.group);
  7735. for (var prop in groupObj) {
  7736. if (groupObj.hasOwnProperty(prop)) {
  7737. this[prop] = groupObj[prop];
  7738. }
  7739. }
  7740. }
  7741. // individual shape properties
  7742. if (properties.shape != undefined) {this.shape = properties.shape;}
  7743. if (properties.image != undefined) {this.image = properties.image;}
  7744. if (properties.radius != undefined) {this.radius = properties.radius;}
  7745. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  7746. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7747. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7748. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7749. if (this.image != undefined) {
  7750. if (this.imagelist) {
  7751. this.imageObj = this.imagelist.load(this.image);
  7752. }
  7753. else {
  7754. throw "No imagelist provided";
  7755. }
  7756. }
  7757. this.xFixed = this.xFixed || (properties.x != undefined);
  7758. this.yFixed = this.yFixed || (properties.y != undefined);
  7759. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  7760. if (this.shape == 'image') {
  7761. this.radiusMin = constants.nodes.widthMin;
  7762. this.radiusMax = constants.nodes.widthMax;
  7763. }
  7764. // choose draw method depending on the shape
  7765. switch (this.shape) {
  7766. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7767. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7768. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7769. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7770. // TODO: add diamond shape
  7771. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7772. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7773. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7774. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7775. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7776. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7777. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7778. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7779. }
  7780. // reset the size of the node, this can be changed
  7781. this._reset();
  7782. };
  7783. /**
  7784. * Parse a color property into an object with border, background, and
  7785. * hightlight colors
  7786. * @param {Object | String} color
  7787. * @return {Object} colorObject
  7788. */
  7789. Node.parseColor = function(color) {
  7790. var c;
  7791. if (util.isString(color)) {
  7792. c = {
  7793. border: color,
  7794. background: color,
  7795. highlight: {
  7796. border: color,
  7797. background: color
  7798. }
  7799. };
  7800. // TODO: automatically generate a nice highlight color
  7801. }
  7802. else {
  7803. c = {};
  7804. c.background = color.background || 'white';
  7805. c.border = color.border || c.background;
  7806. if (util.isString(color.highlight)) {
  7807. c.highlight = {
  7808. border: color.highlight,
  7809. background: color.highlight
  7810. }
  7811. }
  7812. else {
  7813. c.highlight = {};
  7814. c.highlight.background = color.highlight && color.highlight.background || c.background;
  7815. c.highlight.border = color.highlight && color.highlight.border || c.border;
  7816. }
  7817. }
  7818. return c;
  7819. };
  7820. /**
  7821. * select this node
  7822. */
  7823. Node.prototype.select = function() {
  7824. this.selected = true;
  7825. this._reset();
  7826. };
  7827. /**
  7828. * unselect this node
  7829. */
  7830. Node.prototype.unselect = function() {
  7831. this.selected = false;
  7832. this._reset();
  7833. };
  7834. /**
  7835. * Reset the calculated size of the node, forces it to recalculate its size
  7836. * @private
  7837. */
  7838. Node.prototype._reset = function() {
  7839. this.width = undefined;
  7840. this.height = undefined;
  7841. };
  7842. /**
  7843. * get the title of this node.
  7844. * @return {string} title The title of the node, or undefined when no title
  7845. * has been set.
  7846. */
  7847. Node.prototype.getTitle = function() {
  7848. return this.title;
  7849. };
  7850. /**
  7851. * Calculate the distance to the border of the Node
  7852. * @param {CanvasRenderingContext2D} ctx
  7853. * @param {Number} angle Angle in radians
  7854. * @returns {number} distance Distance to the border in pixels
  7855. */
  7856. Node.prototype.distanceToBorder = function (ctx, angle) {
  7857. var borderWidth = 1;
  7858. if (!this.width) {
  7859. this.resize(ctx);
  7860. }
  7861. //noinspection FallthroughInSwitchStatementJS
  7862. switch (this.shape) {
  7863. case 'circle':
  7864. case 'dot':
  7865. return this.radius + borderWidth;
  7866. case 'ellipse':
  7867. var a = this.width / 2;
  7868. var b = this.height / 2;
  7869. var w = (Math.sin(angle) * a);
  7870. var h = (Math.cos(angle) * b);
  7871. return a * b / Math.sqrt(w * w + h * h);
  7872. // TODO: implement distanceToBorder for database
  7873. // TODO: implement distanceToBorder for triangle
  7874. // TODO: implement distanceToBorder for triangleDown
  7875. case 'box':
  7876. case 'image':
  7877. case 'text':
  7878. default:
  7879. if (this.width) {
  7880. return Math.min(
  7881. Math.abs(this.width / 2 / Math.cos(angle)),
  7882. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7883. // TODO: reckon with border radius too in case of box
  7884. }
  7885. else {
  7886. return 0;
  7887. }
  7888. }
  7889. // TODO: implement calculation of distance to border for all shapes
  7890. };
  7891. /**
  7892. * Set forces acting on the node
  7893. * @param {number} fx Force in horizontal direction
  7894. * @param {number} fy Force in vertical direction
  7895. */
  7896. Node.prototype._setForce = function(fx, fy) {
  7897. this.fx = fx;
  7898. this.fy = fy;
  7899. };
  7900. /**
  7901. * Add forces acting on the node
  7902. * @param {number} fx Force in horizontal direction
  7903. * @param {number} fy Force in vertical direction
  7904. * @private
  7905. */
  7906. Node.prototype._addForce = function(fx, fy) {
  7907. this.fx += fx;
  7908. this.fy += fy;
  7909. };
  7910. /**
  7911. * Perform one discrete step for the node
  7912. * @param {number} interval Time interval in seconds
  7913. */
  7914. Node.prototype.discreteStep = function(interval) {
  7915. if (!this.xFixed) {
  7916. var dx = -this.damping * this.vx; // damping force
  7917. var ax = (this.fx + dx) / this.mass; // acceleration
  7918. this.vx += ax / interval; // velocity
  7919. this.x += this.vx / interval; // position
  7920. }
  7921. if (!this.yFixed) {
  7922. var dy = -this.damping * this.vy; // damping force
  7923. var ay = (this.fy + dy) / this.mass; // acceleration
  7924. this.vy += ay / interval; // velocity
  7925. this.y += this.vy / interval; // position
  7926. }
  7927. };
  7928. /**
  7929. * Check if this node has a fixed x and y position
  7930. * @return {boolean} true if fixed, false if not
  7931. */
  7932. Node.prototype.isFixed = function() {
  7933. return (this.xFixed && this.yFixed);
  7934. };
  7935. /**
  7936. * Check if this node is moving
  7937. * @param {number} vmin the minimum velocity considered as "moving"
  7938. * @return {boolean} true if moving, false if it has no velocity
  7939. */
  7940. // TODO: replace this method with calculating the kinetic energy
  7941. Node.prototype.isMoving = function(vmin) {
  7942. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  7943. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  7944. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  7945. };
  7946. /**
  7947. * check if this node is selecte
  7948. * @return {boolean} selected True if node is selected, else false
  7949. */
  7950. Node.prototype.isSelected = function() {
  7951. return this.selected;
  7952. };
  7953. /**
  7954. * Retrieve the value of the node. Can be undefined
  7955. * @return {Number} value
  7956. */
  7957. Node.prototype.getValue = function() {
  7958. return this.value;
  7959. };
  7960. /**
  7961. * Calculate the distance from the nodes location to the given location (x,y)
  7962. * @param {Number} x
  7963. * @param {Number} y
  7964. * @return {Number} value
  7965. */
  7966. Node.prototype.getDistance = function(x, y) {
  7967. var dx = this.x - x,
  7968. dy = this.y - y;
  7969. return Math.sqrt(dx * dx + dy * dy);
  7970. };
  7971. /**
  7972. * Adjust the value range of the node. The node will adjust it's radius
  7973. * based on its value.
  7974. * @param {Number} min
  7975. * @param {Number} max
  7976. */
  7977. Node.prototype.setValueRange = function(min, max) {
  7978. if (!this.radiusFixed && this.value !== undefined) {
  7979. if (max == min) {
  7980. this.radius = (this.radiusMin + this.radiusMax) / 2;
  7981. }
  7982. else {
  7983. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  7984. this.radius = (this.value - min) * scale + this.radiusMin;
  7985. }
  7986. }
  7987. };
  7988. /**
  7989. * Draw this node in the given canvas
  7990. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7991. * @param {CanvasRenderingContext2D} ctx
  7992. */
  7993. Node.prototype.draw = function(ctx) {
  7994. throw "Draw method not initialized for node";
  7995. };
  7996. /**
  7997. * Recalculate the size of this node in the given canvas
  7998. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7999. * @param {CanvasRenderingContext2D} ctx
  8000. */
  8001. Node.prototype.resize = function(ctx) {
  8002. throw "Resize method not initialized for node";
  8003. };
  8004. /**
  8005. * Check if this object is overlapping with the provided object
  8006. * @param {Object} obj an object with parameters left, top, right, bottom
  8007. * @return {boolean} True if location is located on node
  8008. */
  8009. Node.prototype.isOverlappingWith = function(obj) {
  8010. return (this.left < obj.right &&
  8011. this.left + this.width > obj.left &&
  8012. this.top < obj.bottom &&
  8013. this.top + this.height > obj.top);
  8014. };
  8015. Node.prototype._resizeImage = function (ctx) {
  8016. // TODO: pre calculate the image size
  8017. if (!this.width) { // undefined or 0
  8018. var width, height;
  8019. if (this.value) {
  8020. var scale = this.imageObj.height / this.imageObj.width;
  8021. width = this.radius || this.imageObj.width;
  8022. height = this.radius * scale || this.imageObj.height;
  8023. }
  8024. else {
  8025. width = this.imageObj.width;
  8026. height = this.imageObj.height;
  8027. }
  8028. this.width = width;
  8029. this.height = height;
  8030. }
  8031. };
  8032. Node.prototype._drawImage = function (ctx) {
  8033. this._resizeImage(ctx);
  8034. this.left = this.x - this.width / 2;
  8035. this.top = this.y - this.height / 2;
  8036. var yLabel;
  8037. if (this.imageObj) {
  8038. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8039. yLabel = this.y + this.height / 2;
  8040. }
  8041. else {
  8042. // image still loading... just draw the label for now
  8043. yLabel = this.y;
  8044. }
  8045. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8046. };
  8047. Node.prototype._resizeBox = function (ctx) {
  8048. if (!this.width) {
  8049. var margin = 5;
  8050. var textSize = this.getTextSize(ctx);
  8051. this.width = textSize.width + 2 * margin;
  8052. this.height = textSize.height + 2 * margin;
  8053. }
  8054. };
  8055. Node.prototype._drawBox = function (ctx) {
  8056. this._resizeBox(ctx);
  8057. this.left = this.x - this.width / 2;
  8058. this.top = this.y - this.height / 2;
  8059. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8060. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8061. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8062. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8063. ctx.fill();
  8064. ctx.stroke();
  8065. this._label(ctx, this.label, this.x, this.y);
  8066. };
  8067. Node.prototype._resizeDatabase = function (ctx) {
  8068. if (!this.width) {
  8069. var margin = 5;
  8070. var textSize = this.getTextSize(ctx);
  8071. var size = textSize.width + 2 * margin;
  8072. this.width = size;
  8073. this.height = size;
  8074. }
  8075. };
  8076. Node.prototype._drawDatabase = function (ctx) {
  8077. this._resizeDatabase(ctx);
  8078. this.left = this.x - this.width / 2;
  8079. this.top = this.y - this.height / 2;
  8080. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8081. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8082. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8083. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8084. ctx.fill();
  8085. ctx.stroke();
  8086. this._label(ctx, this.label, this.x, this.y);
  8087. };
  8088. Node.prototype._resizeCircle = function (ctx) {
  8089. if (!this.width) {
  8090. var margin = 5;
  8091. var textSize = this.getTextSize(ctx);
  8092. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8093. this.radius = diameter / 2;
  8094. this.width = diameter;
  8095. this.height = diameter;
  8096. }
  8097. };
  8098. Node.prototype._drawCircle = function (ctx) {
  8099. this._resizeCircle(ctx);
  8100. this.left = this.x - this.width / 2;
  8101. this.top = this.y - this.height / 2;
  8102. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8103. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8104. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8105. ctx.circle(this.x, this.y, this.radius);
  8106. ctx.fill();
  8107. ctx.stroke();
  8108. this._label(ctx, this.label, this.x, this.y);
  8109. };
  8110. Node.prototype._resizeEllipse = function (ctx) {
  8111. if (!this.width) {
  8112. var textSize = this.getTextSize(ctx);
  8113. this.width = textSize.width * 1.5;
  8114. this.height = textSize.height * 2;
  8115. if (this.width < this.height) {
  8116. this.width = this.height;
  8117. }
  8118. }
  8119. };
  8120. Node.prototype._drawEllipse = function (ctx) {
  8121. this._resizeEllipse(ctx);
  8122. this.left = this.x - this.width / 2;
  8123. this.top = this.y - this.height / 2;
  8124. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8125. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8126. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8127. ctx.ellipse(this.left, this.top, this.width, this.height);
  8128. ctx.fill();
  8129. ctx.stroke();
  8130. this._label(ctx, this.label, this.x, this.y);
  8131. };
  8132. Node.prototype._drawDot = function (ctx) {
  8133. this._drawShape(ctx, 'circle');
  8134. };
  8135. Node.prototype._drawTriangle = function (ctx) {
  8136. this._drawShape(ctx, 'triangle');
  8137. };
  8138. Node.prototype._drawTriangleDown = function (ctx) {
  8139. this._drawShape(ctx, 'triangleDown');
  8140. };
  8141. Node.prototype._drawSquare = function (ctx) {
  8142. this._drawShape(ctx, 'square');
  8143. };
  8144. Node.prototype._drawStar = function (ctx) {
  8145. this._drawShape(ctx, 'star');
  8146. };
  8147. Node.prototype._resizeShape = function (ctx) {
  8148. if (!this.width) {
  8149. var size = 2 * this.radius;
  8150. this.width = size;
  8151. this.height = size;
  8152. }
  8153. };
  8154. Node.prototype._drawShape = function (ctx, shape) {
  8155. this._resizeShape(ctx);
  8156. this.left = this.x - this.width / 2;
  8157. this.top = this.y - this.height / 2;
  8158. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8159. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8160. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8161. ctx[shape](this.x, this.y, this.radius);
  8162. ctx.fill();
  8163. ctx.stroke();
  8164. if (this.label) {
  8165. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8166. }
  8167. };
  8168. Node.prototype._resizeText = function (ctx) {
  8169. if (!this.width) {
  8170. var margin = 5;
  8171. var textSize = this.getTextSize(ctx);
  8172. this.width = textSize.width + 2 * margin;
  8173. this.height = textSize.height + 2 * margin;
  8174. }
  8175. };
  8176. Node.prototype._drawText = function (ctx) {
  8177. this._resizeText(ctx);
  8178. this.left = this.x - this.width / 2;
  8179. this.top = this.y - this.height / 2;
  8180. this._label(ctx, this.label, this.x, this.y);
  8181. };
  8182. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8183. if (text) {
  8184. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8185. ctx.fillStyle = this.fontColor || "black";
  8186. ctx.textAlign = align || "center";
  8187. ctx.textBaseline = baseline || "middle";
  8188. var lines = text.split('\n'),
  8189. lineCount = lines.length,
  8190. fontSize = (this.fontSize + 4),
  8191. yLine = y + (1 - lineCount) / 2 * fontSize;
  8192. for (var i = 0; i < lineCount; i++) {
  8193. ctx.fillText(lines[i], x, yLine);
  8194. yLine += fontSize;
  8195. }
  8196. }
  8197. };
  8198. Node.prototype.getTextSize = function(ctx) {
  8199. if (this.label != undefined) {
  8200. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8201. var lines = this.label.split('\n'),
  8202. height = (this.fontSize + 4) * lines.length,
  8203. width = 0;
  8204. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8205. width = Math.max(width, ctx.measureText(lines[i]).width);
  8206. }
  8207. return {"width": width, "height": height};
  8208. }
  8209. else {
  8210. return {"width": 0, "height": 0};
  8211. }
  8212. };
  8213. /**
  8214. * @class Edge
  8215. *
  8216. * A edge connects two nodes
  8217. * @param {Object} properties Object with properties. Must contain
  8218. * At least properties from and to.
  8219. * Available properties: from (number),
  8220. * to (number), label (string, color (string),
  8221. * width (number), style (string),
  8222. * length (number), title (string)
  8223. * @param {Graph} graph A graph object, used to find and edge to
  8224. * nodes.
  8225. * @param {Object} constants An object with default values for
  8226. * example for the color
  8227. */
  8228. function Edge (properties, graph, constants) {
  8229. if (!graph) {
  8230. throw "No graph provided";
  8231. }
  8232. this.graph = graph;
  8233. // initialize constants
  8234. this.widthMin = constants.edges.widthMin;
  8235. this.widthMax = constants.edges.widthMax;
  8236. // initialize variables
  8237. this.id = undefined;
  8238. this.fromId = undefined;
  8239. this.toId = undefined;
  8240. this.style = constants.edges.style;
  8241. this.title = undefined;
  8242. this.width = constants.edges.width;
  8243. this.value = undefined;
  8244. this.length = constants.edges.length;
  8245. this.from = null; // a node
  8246. this.to = null; // a node
  8247. this.connected = false;
  8248. // Added to support dashed lines
  8249. // David Jordan
  8250. // 2012-08-08
  8251. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8252. this.stiffness = undefined; // depends on the length of the edge
  8253. this.color = constants.edges.color;
  8254. this.widthFixed = false;
  8255. this.lengthFixed = false;
  8256. this.setProperties(properties, constants);
  8257. }
  8258. /**
  8259. * Set or overwrite properties for the edge
  8260. * @param {Object} properties an object with properties
  8261. * @param {Object} constants and object with default, global properties
  8262. */
  8263. Edge.prototype.setProperties = function(properties, constants) {
  8264. if (!properties) {
  8265. return;
  8266. }
  8267. if (properties.from != undefined) {this.fromId = properties.from;}
  8268. if (properties.to != undefined) {this.toId = properties.to;}
  8269. if (properties.id != undefined) {this.id = properties.id;}
  8270. if (properties.style != undefined) {this.style = properties.style;}
  8271. if (properties.label != undefined) {this.label = properties.label;}
  8272. if (this.label) {
  8273. this.fontSize = constants.edges.fontSize;
  8274. this.fontFace = constants.edges.fontFace;
  8275. this.fontColor = constants.edges.fontColor;
  8276. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  8277. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  8278. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  8279. }
  8280. if (properties.title != undefined) {this.title = properties.title;}
  8281. if (properties.width != undefined) {this.width = properties.width;}
  8282. if (properties.value != undefined) {this.value = properties.value;}
  8283. if (properties.length != undefined) {this.length = properties.length;}
  8284. // Added to support dashed lines
  8285. // David Jordan
  8286. // 2012-08-08
  8287. if (properties.dash) {
  8288. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  8289. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  8290. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  8291. }
  8292. if (properties.color != undefined) {this.color = properties.color;}
  8293. // A node is connected when it has a from and to node.
  8294. this.connect();
  8295. this.widthFixed = this.widthFixed || (properties.width != undefined);
  8296. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  8297. this.stiffness = 1 / this.length;
  8298. // set draw method based on style
  8299. switch (this.style) {
  8300. case 'line': this.draw = this._drawLine; break;
  8301. case 'arrow': this.draw = this._drawArrow; break;
  8302. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8303. case 'dash-line': this.draw = this._drawDashLine; break;
  8304. default: this.draw = this._drawLine; break;
  8305. }
  8306. };
  8307. /**
  8308. * Connect an edge to its nodes
  8309. */
  8310. Edge.prototype.connect = function () {
  8311. this.disconnect();
  8312. this.from = this.graph.nodes[this.fromId] || null;
  8313. this.to = this.graph.nodes[this.toId] || null;
  8314. this.connected = (this.from && this.to);
  8315. if (this.connected) {
  8316. this.from.attachEdge(this);
  8317. this.to.attachEdge(this);
  8318. }
  8319. else {
  8320. if (this.from) {
  8321. this.from.detachEdge(this);
  8322. }
  8323. if (this.to) {
  8324. this.to.detachEdge(this);
  8325. }
  8326. }
  8327. };
  8328. /**
  8329. * Disconnect an edge from its nodes
  8330. */
  8331. Edge.prototype.disconnect = function () {
  8332. if (this.from) {
  8333. this.from.detachEdge(this);
  8334. this.from = null;
  8335. }
  8336. if (this.to) {
  8337. this.to.detachEdge(this);
  8338. this.to = null;
  8339. }
  8340. this.connected = false;
  8341. };
  8342. /**
  8343. * get the title of this edge.
  8344. * @return {string} title The title of the edge, or undefined when no title
  8345. * has been set.
  8346. */
  8347. Edge.prototype.getTitle = function() {
  8348. return this.title;
  8349. };
  8350. /**
  8351. * Retrieve the value of the edge. Can be undefined
  8352. * @return {Number} value
  8353. */
  8354. Edge.prototype.getValue = function() {
  8355. return this.value;
  8356. };
  8357. /**
  8358. * Adjust the value range of the edge. The edge will adjust it's width
  8359. * based on its value.
  8360. * @param {Number} min
  8361. * @param {Number} max
  8362. */
  8363. Edge.prototype.setValueRange = function(min, max) {
  8364. if (!this.widthFixed && this.value !== undefined) {
  8365. var scale = (this.widthMax - this.widthMin) / (max - min);
  8366. this.width = (this.value - min) * scale + this.widthMin;
  8367. }
  8368. };
  8369. /**
  8370. * Redraw a edge
  8371. * Draw this edge in the given canvas
  8372. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8373. * @param {CanvasRenderingContext2D} ctx
  8374. */
  8375. Edge.prototype.draw = function(ctx) {
  8376. throw "Method draw not initialized in edge";
  8377. };
  8378. /**
  8379. * Check if this object is overlapping with the provided object
  8380. * @param {Object} obj an object with parameters left, top
  8381. * @return {boolean} True if location is located on the edge
  8382. */
  8383. Edge.prototype.isOverlappingWith = function(obj) {
  8384. var distMax = 10;
  8385. var xFrom = this.from.x;
  8386. var yFrom = this.from.y;
  8387. var xTo = this.to.x;
  8388. var yTo = this.to.y;
  8389. var xObj = obj.left;
  8390. var yObj = obj.top;
  8391. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8392. return (dist < distMax);
  8393. };
  8394. /**
  8395. * Redraw a edge as a line
  8396. * Draw this edge in the given canvas
  8397. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8398. * @param {CanvasRenderingContext2D} ctx
  8399. * @private
  8400. */
  8401. Edge.prototype._drawLine = function(ctx) {
  8402. // set style
  8403. ctx.strokeStyle = this.color;
  8404. ctx.lineWidth = this._getLineWidth();
  8405. var point;
  8406. if (this.from != this.to) {
  8407. // draw line
  8408. this._line(ctx);
  8409. // draw label
  8410. if (this.label) {
  8411. point = this._pointOnLine(0.5);
  8412. this._label(ctx, this.label, point.x, point.y);
  8413. }
  8414. }
  8415. else {
  8416. var x, y;
  8417. var radius = this.length / 4;
  8418. var node = this.from;
  8419. if (!node.width) {
  8420. node.resize(ctx);
  8421. }
  8422. if (node.width > node.height) {
  8423. x = node.x + node.width / 2;
  8424. y = node.y - radius;
  8425. }
  8426. else {
  8427. x = node.x + radius;
  8428. y = node.y - node.height / 2;
  8429. }
  8430. this._circle(ctx, x, y, radius);
  8431. point = this._pointOnCircle(x, y, radius, 0.5);
  8432. this._label(ctx, this.label, point.x, point.y);
  8433. }
  8434. };
  8435. /**
  8436. * Get the line width of the edge. Depends on width and whether one of the
  8437. * connected nodes is selected.
  8438. * @return {Number} width
  8439. * @private
  8440. */
  8441. Edge.prototype._getLineWidth = function() {
  8442. if (this.from.selected || this.to.selected) {
  8443. return Math.min(this.width * 2, this.widthMax);
  8444. }
  8445. else {
  8446. return this.width;
  8447. }
  8448. };
  8449. /**
  8450. * Draw a line between two nodes
  8451. * @param {CanvasRenderingContext2D} ctx
  8452. * @private
  8453. */
  8454. Edge.prototype._line = function (ctx) {
  8455. // draw a straight line
  8456. ctx.beginPath();
  8457. ctx.moveTo(this.from.x, this.from.y);
  8458. ctx.lineTo(this.to.x, this.to.y);
  8459. ctx.stroke();
  8460. };
  8461. /**
  8462. * Draw a line from a node to itself, a circle
  8463. * @param {CanvasRenderingContext2D} ctx
  8464. * @param {Number} x
  8465. * @param {Number} y
  8466. * @param {Number} radius
  8467. * @private
  8468. */
  8469. Edge.prototype._circle = function (ctx, x, y, radius) {
  8470. // draw a circle
  8471. ctx.beginPath();
  8472. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8473. ctx.stroke();
  8474. };
  8475. /**
  8476. * Draw label with white background and with the middle at (x, y)
  8477. * @param {CanvasRenderingContext2D} ctx
  8478. * @param {String} text
  8479. * @param {Number} x
  8480. * @param {Number} y
  8481. * @private
  8482. */
  8483. Edge.prototype._label = function (ctx, text, x, y) {
  8484. if (text) {
  8485. // TODO: cache the calculated size
  8486. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8487. this.fontSize + "px " + this.fontFace;
  8488. ctx.fillStyle = 'white';
  8489. var width = ctx.measureText(text).width;
  8490. var height = this.fontSize;
  8491. var left = x - width / 2;
  8492. var top = y - height / 2;
  8493. ctx.fillRect(left, top, width, height);
  8494. // draw text
  8495. ctx.fillStyle = this.fontColor || "black";
  8496. ctx.textAlign = "left";
  8497. ctx.textBaseline = "top";
  8498. ctx.fillText(text, left, top);
  8499. }
  8500. };
  8501. /**
  8502. * Redraw a edge as a dashed line
  8503. * Draw this edge in the given canvas
  8504. * @author David Jordan
  8505. * @date 2012-08-08
  8506. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8507. * @param {CanvasRenderingContext2D} ctx
  8508. * @private
  8509. */
  8510. Edge.prototype._drawDashLine = function(ctx) {
  8511. // set style
  8512. ctx.strokeStyle = this.color;
  8513. ctx.lineWidth = this._getLineWidth();
  8514. // draw dashed line
  8515. ctx.beginPath();
  8516. ctx.lineCap = 'round';
  8517. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  8518. {
  8519. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8520. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8521. }
  8522. 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
  8523. {
  8524. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8525. [this.dash.length,this.dash.gap]);
  8526. }
  8527. else //If all else fails draw a line
  8528. {
  8529. ctx.moveTo(this.from.x, this.from.y);
  8530. ctx.lineTo(this.to.x, this.to.y);
  8531. }
  8532. ctx.stroke();
  8533. // draw label
  8534. if (this.label) {
  8535. var point = this._pointOnLine(0.5);
  8536. this._label(ctx, this.label, point.x, point.y);
  8537. }
  8538. };
  8539. /**
  8540. * Get a point on a line
  8541. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8542. * @return {Object} point
  8543. * @private
  8544. */
  8545. Edge.prototype._pointOnLine = function (percentage) {
  8546. return {
  8547. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  8548. y: (1 - percentage) * this.from.y + percentage * this.to.y
  8549. }
  8550. };
  8551. /**
  8552. * Get a point on a circle
  8553. * @param {Number} x
  8554. * @param {Number} y
  8555. * @param {Number} radius
  8556. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8557. * @return {Object} point
  8558. * @private
  8559. */
  8560. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  8561. var angle = (percentage - 3/8) * 2 * Math.PI;
  8562. return {
  8563. x: x + radius * Math.cos(angle),
  8564. y: y - radius * Math.sin(angle)
  8565. }
  8566. };
  8567. /**
  8568. * Redraw a edge as a line with an arrow halfway the line
  8569. * Draw this edge in the given canvas
  8570. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8571. * @param {CanvasRenderingContext2D} ctx
  8572. * @private
  8573. */
  8574. Edge.prototype._drawArrowCenter = function(ctx) {
  8575. var point;
  8576. // set style
  8577. ctx.strokeStyle = this.color;
  8578. ctx.fillStyle = this.color;
  8579. ctx.lineWidth = this._getLineWidth();
  8580. if (this.from != this.to) {
  8581. // draw line
  8582. this._line(ctx);
  8583. // draw an arrow halfway the line
  8584. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8585. var length = 10 + 5 * this.width; // TODO: make customizable?
  8586. point = this._pointOnLine(0.5);
  8587. ctx.arrow(point.x, point.y, angle, length);
  8588. ctx.fill();
  8589. ctx.stroke();
  8590. // draw label
  8591. if (this.label) {
  8592. point = this._pointOnLine(0.5);
  8593. this._label(ctx, this.label, point.x, point.y);
  8594. }
  8595. }
  8596. else {
  8597. // draw circle
  8598. var x, y;
  8599. var radius = this.length / 4;
  8600. var node = this.from;
  8601. if (!node.width) {
  8602. node.resize(ctx);
  8603. }
  8604. if (node.width > node.height) {
  8605. x = node.x + node.width / 2;
  8606. y = node.y - radius;
  8607. }
  8608. else {
  8609. x = node.x + radius;
  8610. y = node.y - node.height / 2;
  8611. }
  8612. this._circle(ctx, x, y, radius);
  8613. // draw all arrows
  8614. var angle = 0.2 * Math.PI;
  8615. var length = 10 + 5 * this.width; // TODO: make customizable?
  8616. point = this._pointOnCircle(x, y, radius, 0.5);
  8617. ctx.arrow(point.x, point.y, angle, length);
  8618. ctx.fill();
  8619. ctx.stroke();
  8620. // draw label
  8621. if (this.label) {
  8622. point = this._pointOnCircle(x, y, radius, 0.5);
  8623. this._label(ctx, this.label, point.x, point.y);
  8624. }
  8625. }
  8626. };
  8627. /**
  8628. * Redraw a edge as a line with an arrow
  8629. * Draw this edge in the given canvas
  8630. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8631. * @param {CanvasRenderingContext2D} ctx
  8632. * @private
  8633. */
  8634. Edge.prototype._drawArrow = function(ctx) {
  8635. // set style
  8636. ctx.strokeStyle = this.color;
  8637. ctx.fillStyle = this.color;
  8638. ctx.lineWidth = this._getLineWidth();
  8639. // draw line
  8640. var angle, length;
  8641. if (this.from != this.to) {
  8642. // calculate length and angle of the line
  8643. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8644. var dx = (this.to.x - this.from.x);
  8645. var dy = (this.to.y - this.from.y);
  8646. var lEdge = Math.sqrt(dx * dx + dy * dy);
  8647. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  8648. var pFrom = (lEdge - lFrom) / lEdge;
  8649. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  8650. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  8651. var lTo = this.to.distanceToBorder(ctx, angle);
  8652. var pTo = (lEdge - lTo) / lEdge;
  8653. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  8654. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  8655. ctx.beginPath();
  8656. ctx.moveTo(xFrom, yFrom);
  8657. ctx.lineTo(xTo, yTo);
  8658. ctx.stroke();
  8659. // draw arrow at the end of the line
  8660. length = 10 + 5 * this.width; // TODO: make customizable?
  8661. ctx.arrow(xTo, yTo, angle, length);
  8662. ctx.fill();
  8663. ctx.stroke();
  8664. // draw label
  8665. if (this.label) {
  8666. var point = this._pointOnLine(0.5);
  8667. this._label(ctx, this.label, point.x, point.y);
  8668. }
  8669. }
  8670. else {
  8671. // draw circle
  8672. var node = this.from;
  8673. var x, y, arrow;
  8674. var radius = this.length / 4;
  8675. if (!node.width) {
  8676. node.resize(ctx);
  8677. }
  8678. if (node.width > node.height) {
  8679. x = node.x + node.width / 2;
  8680. y = node.y - radius;
  8681. arrow = {
  8682. x: x,
  8683. y: node.y,
  8684. angle: 0.9 * Math.PI
  8685. };
  8686. }
  8687. else {
  8688. x = node.x + radius;
  8689. y = node.y - node.height / 2;
  8690. arrow = {
  8691. x: node.x,
  8692. y: y,
  8693. angle: 0.6 * Math.PI
  8694. };
  8695. }
  8696. ctx.beginPath();
  8697. // TODO: do not draw a circle, but an arc
  8698. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  8699. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8700. ctx.stroke();
  8701. // draw all arrows
  8702. length = 10 + 5 * this.width; // TODO: make customizable?
  8703. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  8704. ctx.fill();
  8705. ctx.stroke();
  8706. // draw label
  8707. if (this.label) {
  8708. point = this._pointOnCircle(x, y, radius, 0.5);
  8709. this._label(ctx, this.label, point.x, point.y);
  8710. }
  8711. }
  8712. };
  8713. /**
  8714. * Calculate the distance between a point (x3,y3) and a line segment from
  8715. * (x1,y1) to (x2,y2).
  8716. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  8717. * @param {number} x1
  8718. * @param {number} y1
  8719. * @param {number} x2
  8720. * @param {number} y2
  8721. * @param {number} x3
  8722. * @param {number} y3
  8723. * @private
  8724. */
  8725. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  8726. var px = x2-x1,
  8727. py = y2-y1,
  8728. something = px*px + py*py,
  8729. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  8730. if (u > 1) {
  8731. u = 1;
  8732. }
  8733. else if (u < 0) {
  8734. u = 0;
  8735. }
  8736. var x = x1 + u * px,
  8737. y = y1 + u * py,
  8738. dx = x - x3,
  8739. dy = y - y3;
  8740. //# Note: If the actual distance does not matter,
  8741. //# if you only want to compare what this function
  8742. //# returns to other results of this function, you
  8743. //# can just return the squared distance instead
  8744. //# (i.e. remove the sqrt) to gain a little performance
  8745. return Math.sqrt(dx*dx + dy*dy);
  8746. };
  8747. /**
  8748. * Popup is a class to create a popup window with some text
  8749. * @param {Element} container The container object.
  8750. * @param {Number} [x]
  8751. * @param {Number} [y]
  8752. * @param {String} [text]
  8753. */
  8754. function Popup(container, x, y, text) {
  8755. if (container) {
  8756. this.container = container;
  8757. }
  8758. else {
  8759. this.container = document.body;
  8760. }
  8761. this.x = 0;
  8762. this.y = 0;
  8763. this.padding = 5;
  8764. if (x !== undefined && y !== undefined ) {
  8765. this.setPosition(x, y);
  8766. }
  8767. if (text !== undefined) {
  8768. this.setText(text);
  8769. }
  8770. // create the frame
  8771. this.frame = document.createElement("div");
  8772. var style = this.frame.style;
  8773. style.position = "absolute";
  8774. style.visibility = "hidden";
  8775. style.border = "1px solid #666";
  8776. style.color = "black";
  8777. style.padding = this.padding + "px";
  8778. style.backgroundColor = "#FFFFC6";
  8779. style.borderRadius = "3px";
  8780. style.MozBorderRadius = "3px";
  8781. style.WebkitBorderRadius = "3px";
  8782. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  8783. style.whiteSpace = "nowrap";
  8784. this.container.appendChild(this.frame);
  8785. };
  8786. /**
  8787. * @param {number} x Horizontal position of the popup window
  8788. * @param {number} y Vertical position of the popup window
  8789. */
  8790. Popup.prototype.setPosition = function(x, y) {
  8791. this.x = parseInt(x);
  8792. this.y = parseInt(y);
  8793. };
  8794. /**
  8795. * Set the text for the popup window. This can be HTML code
  8796. * @param {string} text
  8797. */
  8798. Popup.prototype.setText = function(text) {
  8799. this.frame.innerHTML = text;
  8800. };
  8801. /**
  8802. * Show the popup window
  8803. * @param {boolean} show Optional. Show or hide the window
  8804. */
  8805. Popup.prototype.show = function (show) {
  8806. if (show === undefined) {
  8807. show = true;
  8808. }
  8809. if (show) {
  8810. var height = this.frame.clientHeight;
  8811. var width = this.frame.clientWidth;
  8812. var maxHeight = this.frame.parentNode.clientHeight;
  8813. var maxWidth = this.frame.parentNode.clientWidth;
  8814. var top = (this.y - height);
  8815. if (top + height + this.padding > maxHeight) {
  8816. top = maxHeight - height - this.padding;
  8817. }
  8818. if (top < this.padding) {
  8819. top = this.padding;
  8820. }
  8821. var left = this.x;
  8822. if (left + width + this.padding > maxWidth) {
  8823. left = maxWidth - width - this.padding;
  8824. }
  8825. if (left < this.padding) {
  8826. left = this.padding;
  8827. }
  8828. this.frame.style.left = left + "px";
  8829. this.frame.style.top = top + "px";
  8830. this.frame.style.visibility = "visible";
  8831. }
  8832. else {
  8833. this.hide();
  8834. }
  8835. };
  8836. /**
  8837. * Hide the popup window
  8838. */
  8839. Popup.prototype.hide = function () {
  8840. this.frame.style.visibility = "hidden";
  8841. };
  8842. /**
  8843. * @class Groups
  8844. * This class can store groups and properties specific for groups.
  8845. */
  8846. Groups = function () {
  8847. this.clear();
  8848. this.defaultIndex = 0;
  8849. };
  8850. /**
  8851. * default constants for group colors
  8852. */
  8853. Groups.DEFAULT = [
  8854. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  8855. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  8856. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  8857. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  8858. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  8859. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  8860. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  8861. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  8862. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  8863. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  8864. ];
  8865. /**
  8866. * Clear all groups
  8867. */
  8868. Groups.prototype.clear = function () {
  8869. this.groups = {};
  8870. this.groups.length = function()
  8871. {
  8872. var i = 0;
  8873. for ( var p in this ) {
  8874. if (this.hasOwnProperty(p)) {
  8875. i++;
  8876. }
  8877. }
  8878. return i;
  8879. }
  8880. };
  8881. /**
  8882. * get group properties of a groupname. If groupname is not found, a new group
  8883. * is added.
  8884. * @param {*} groupname Can be a number, string, Date, etc.
  8885. * @return {Object} group The created group, containing all group properties
  8886. */
  8887. Groups.prototype.get = function (groupname) {
  8888. var group = this.groups[groupname];
  8889. if (group == undefined) {
  8890. // create new group
  8891. var index = this.defaultIndex % Groups.DEFAULT.length;
  8892. this.defaultIndex++;
  8893. group = {};
  8894. group.color = Groups.DEFAULT[index];
  8895. this.groups[groupname] = group;
  8896. }
  8897. return group;
  8898. };
  8899. /**
  8900. * Add a custom group style
  8901. * @param {String} groupname
  8902. * @param {Object} style An object containing borderColor,
  8903. * backgroundColor, etc.
  8904. * @return {Object} group The created group object
  8905. */
  8906. Groups.prototype.add = function (groupname, style) {
  8907. this.groups[groupname] = style;
  8908. if (style.color) {
  8909. style.color = Node.parseColor(style.color);
  8910. }
  8911. return style;
  8912. };
  8913. /**
  8914. * @class Images
  8915. * This class loads images and keeps them stored.
  8916. */
  8917. Images = function () {
  8918. this.images = {};
  8919. this.callback = undefined;
  8920. };
  8921. /**
  8922. * Set an onload callback function. This will be called each time an image
  8923. * is loaded
  8924. * @param {function} callback
  8925. */
  8926. Images.prototype.setOnloadCallback = function(callback) {
  8927. this.callback = callback;
  8928. };
  8929. /**
  8930. *
  8931. * @param {string} url Url of the image
  8932. * @return {Image} img The image object
  8933. */
  8934. Images.prototype.load = function(url) {
  8935. var img = this.images[url];
  8936. if (img == undefined) {
  8937. // create the image
  8938. var images = this;
  8939. img = new Image();
  8940. this.images[url] = img;
  8941. img.onload = function() {
  8942. if (images.callback) {
  8943. images.callback(this);
  8944. }
  8945. };
  8946. img.src = url;
  8947. }
  8948. return img;
  8949. };
  8950. /**
  8951. * @constructor Graph
  8952. * Create a graph visualization, displaying nodes and edges.
  8953. *
  8954. * @param {Element} container The DOM element in which the Graph will
  8955. * be created. Normally a div element.
  8956. * @param {Object} data An object containing parameters
  8957. * {Array} nodes
  8958. * {Array} edges
  8959. * @param {Object} options Options
  8960. */
  8961. function Graph (container, data, options) {
  8962. // create variables and set default values
  8963. this.containerElement = container;
  8964. this.width = '100%';
  8965. this.height = '100%';
  8966. this.refreshRate = 50; // milliseconds
  8967. this.stabilize = true; // stabilize before displaying the graph
  8968. this.selectable = true;
  8969. // set constant values
  8970. this.constants = {
  8971. nodes: {
  8972. radiusMin: 5,
  8973. radiusMax: 20,
  8974. radius: 5,
  8975. distance: 100, // px
  8976. shape: 'ellipse',
  8977. image: undefined,
  8978. widthMin: 16, // px
  8979. widthMax: 64, // px
  8980. fontColor: 'black',
  8981. fontSize: 14, // px
  8982. //fontFace: verdana,
  8983. fontFace: 'arial',
  8984. color: {
  8985. border: '#2B7CE9',
  8986. background: '#97C2FC',
  8987. highlight: {
  8988. border: '#2B7CE9',
  8989. background: '#D2E5FF'
  8990. }
  8991. },
  8992. borderColor: '#2B7CE9',
  8993. backgroundColor: '#97C2FC',
  8994. highlightColor: '#D2E5FF',
  8995. group: undefined
  8996. },
  8997. edges: {
  8998. widthMin: 1,
  8999. widthMax: 15,
  9000. width: 1,
  9001. style: 'line',
  9002. color: '#343434',
  9003. fontColor: '#343434',
  9004. fontSize: 14, // px
  9005. fontFace: 'arial',
  9006. //distance: 100, //px
  9007. length: 100, // px
  9008. dash: {
  9009. length: 10,
  9010. gap: 5,
  9011. altLength: undefined
  9012. }
  9013. },
  9014. minForce: 0.05,
  9015. minVelocity: 0.02, // px/s
  9016. maxIterations: 1000 // maximum number of iteration to stabilize
  9017. };
  9018. var graph = this;
  9019. this.nodes = {}; // object with Node objects
  9020. this.edges = {}; // object with Edge objects
  9021. // TODO: create a counter to keep track on the number of nodes having values
  9022. // TODO: create a counter to keep track on the number of nodes currently moving
  9023. // TODO: create a counter to keep track on the number of edges having values
  9024. this.nodesData = null; // A DataSet or DataView
  9025. this.edgesData = null; // A DataSet or DataView
  9026. // create event listeners used to subscribe on the DataSets of the nodes and edges
  9027. var me = this;
  9028. this.nodesListeners = {
  9029. 'add': function (event, params) {
  9030. me._addNodes(params.items);
  9031. me.start();
  9032. },
  9033. 'update': function (event, params) {
  9034. me._updateNodes(params.items);
  9035. me.start();
  9036. },
  9037. 'remove': function (event, params) {
  9038. me._removeNodes(params.items);
  9039. me.start();
  9040. }
  9041. };
  9042. this.edgesListeners = {
  9043. 'add': function (event, params) {
  9044. me._addEdges(params.items);
  9045. me.start();
  9046. },
  9047. 'update': function (event, params) {
  9048. me._updateEdges(params.items);
  9049. me.start();
  9050. },
  9051. 'remove': function (event, params) {
  9052. me._removeEdges(params.items);
  9053. me.start();
  9054. }
  9055. };
  9056. this.groups = new Groups(); // object with groups
  9057. this.images = new Images(); // object with images
  9058. this.images.setOnloadCallback(function () {
  9059. graph._redraw();
  9060. });
  9061. // properties of the data
  9062. this.moving = false; // True if any of the nodes have an undefined position
  9063. this.selection = [];
  9064. this.timer = undefined;
  9065. // create a frame and canvas
  9066. this._create();
  9067. // apply options
  9068. this.setOptions(options);
  9069. // draw data
  9070. this.setData(data);
  9071. }
  9072. /**
  9073. * Set nodes and edges, and optionally options as well.
  9074. *
  9075. * @param {Object} data Object containing parameters:
  9076. * {Array | DataSet | DataView} [nodes] Array with nodes
  9077. * {Array | DataSet | DataView} [edges] Array with edges
  9078. * {String} [dot] String containing data in DOT format
  9079. * {Options} [options] Object with options
  9080. */
  9081. Graph.prototype.setData = function(data) {
  9082. if (data && data.dot && (data.nodes || data.edges)) {
  9083. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  9084. ' parameter pair "nodes" and "edges", but not both.');
  9085. }
  9086. // set options
  9087. this.setOptions(data && data.options);
  9088. // set all data
  9089. if (data && data.dot) {
  9090. // parse DOT file
  9091. if(data && data.dot) {
  9092. var dotData = vis.util.DOTToGraph(data.dot);
  9093. this.setData(dotData);
  9094. return;
  9095. }
  9096. }
  9097. else {
  9098. this._setNodes(data && data.nodes);
  9099. this._setEdges(data && data.edges);
  9100. }
  9101. // find a stable position or start animating to a stable position
  9102. if (this.stabilize) {
  9103. this._doStabilize();
  9104. }
  9105. this.start();
  9106. };
  9107. /**
  9108. * Set options
  9109. * @param {Object} options
  9110. */
  9111. Graph.prototype.setOptions = function (options) {
  9112. if (options) {
  9113. // retrieve parameter values
  9114. if (options.width != undefined) {this.width = options.width;}
  9115. if (options.height != undefined) {this.height = options.height;}
  9116. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  9117. if (options.selectable != undefined) {this.selectable = options.selectable;}
  9118. // TODO: work out these options and document them
  9119. if (options.edges) {
  9120. for (var prop in options.edges) {
  9121. if (options.edges.hasOwnProperty(prop)) {
  9122. this.constants.edges[prop] = options.edges[prop];
  9123. }
  9124. }
  9125. if (options.edges.length != undefined &&
  9126. options.nodes && options.nodes.distance == undefined) {
  9127. this.constants.edges.length = options.edges.length;
  9128. this.constants.nodes.distance = options.edges.length * 1.25;
  9129. }
  9130. if (!options.edges.fontColor) {
  9131. this.constants.edges.fontColor = options.edges.color;
  9132. }
  9133. // Added to support dashed lines
  9134. // David Jordan
  9135. // 2012-08-08
  9136. if (options.edges.dash) {
  9137. if (options.edges.dash.length != undefined) {
  9138. this.constants.edges.dash.length = options.edges.dash.length;
  9139. }
  9140. if (options.edges.dash.gap != undefined) {
  9141. this.constants.edges.dash.gap = options.edges.dash.gap;
  9142. }
  9143. if (options.edges.dash.altLength != undefined) {
  9144. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  9145. }
  9146. }
  9147. }
  9148. if (options.nodes) {
  9149. for (prop in options.nodes) {
  9150. if (options.nodes.hasOwnProperty(prop)) {
  9151. this.constants.nodes[prop] = options.nodes[prop];
  9152. }
  9153. }
  9154. if (options.nodes.color) {
  9155. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  9156. }
  9157. /*
  9158. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  9159. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  9160. */
  9161. }
  9162. if (options.groups) {
  9163. for (var groupname in options.groups) {
  9164. if (options.groups.hasOwnProperty(groupname)) {
  9165. var group = options.groups[groupname];
  9166. this.groups.add(groupname, group);
  9167. }
  9168. }
  9169. }
  9170. }
  9171. this.setSize(this.width, this.height);
  9172. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  9173. this._setScale(1);
  9174. };
  9175. /**
  9176. * fire an event
  9177. * @param {String} event The name of an event, for example 'select'
  9178. * @param {Object} params Optional object with event parameters
  9179. * @private
  9180. */
  9181. Graph.prototype._trigger = function (event, params) {
  9182. events.trigger(this, event, params);
  9183. };
  9184. /**
  9185. * Create the main frame for the Graph.
  9186. * This function is executed once when a Graph object is created. The frame
  9187. * contains a canvas, and this canvas contains all objects like the axis and
  9188. * nodes.
  9189. * @private
  9190. */
  9191. Graph.prototype._create = function () {
  9192. // remove all elements from the container element.
  9193. while (this.containerElement.hasChildNodes()) {
  9194. this.containerElement.removeChild(this.containerElement.firstChild);
  9195. }
  9196. this.frame = document.createElement('div');
  9197. this.frame.className = 'graph-frame';
  9198. this.frame.style.position = 'relative';
  9199. this.frame.style.overflow = 'hidden';
  9200. // create the graph canvas (HTML canvas element)
  9201. this.frame.canvas = document.createElement( 'canvas' );
  9202. this.frame.canvas.style.position = 'relative';
  9203. this.frame.appendChild(this.frame.canvas);
  9204. if (!this.frame.canvas.getContext) {
  9205. var noCanvas = document.createElement( 'DIV' );
  9206. noCanvas.style.color = 'red';
  9207. noCanvas.style.fontWeight = 'bold' ;
  9208. noCanvas.style.padding = '10px';
  9209. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  9210. this.frame.canvas.appendChild(noCanvas);
  9211. }
  9212. var me = this;
  9213. this.drag = {};
  9214. this.pinch = {};
  9215. this.hammer = Hammer(this.frame.canvas, {
  9216. prevent_default: true
  9217. });
  9218. this.hammer.on('tap', me._onTap.bind(me) );
  9219. this.hammer.on('hold', me._onHold.bind(me) );
  9220. this.hammer.on('pinch', me._onPinch.bind(me) );
  9221. this.hammer.on('touch', me._onTouch.bind(me) );
  9222. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  9223. this.hammer.on('drag', me._onDrag.bind(me) );
  9224. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  9225. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  9226. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  9227. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  9228. // add the frame to the container element
  9229. this.containerElement.appendChild(this.frame);
  9230. };
  9231. /**
  9232. *
  9233. * @param {{x: Number, y: Number}} pointer
  9234. * @return {Number | null} node
  9235. * @private
  9236. */
  9237. Graph.prototype._getNodeAt = function (pointer) {
  9238. var x = this._canvasToX(pointer.x);
  9239. var y = this._canvasToY(pointer.y);
  9240. var obj = {
  9241. left: x,
  9242. top: y,
  9243. right: x,
  9244. bottom: y
  9245. };
  9246. // if there are overlapping nodes, select the last one, this is the
  9247. // one which is drawn on top of the others
  9248. var overlappingNodes = this._getNodesOverlappingWith(obj);
  9249. return (overlappingNodes.length > 0) ?
  9250. overlappingNodes[overlappingNodes.length - 1] : null;
  9251. };
  9252. /**
  9253. * Get the pointer location from a touch location
  9254. * @param {{pageX: Number, pageY: Number}} touch
  9255. * @return {{x: Number, y: Number}} pointer
  9256. * @private
  9257. */
  9258. Graph.prototype._getPointer = function (touch) {
  9259. return {
  9260. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  9261. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  9262. };
  9263. };
  9264. /**
  9265. * On start of a touch gesture, store the pointer
  9266. * @param event
  9267. * @private
  9268. */
  9269. Graph.prototype._onTouch = function (event) {
  9270. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  9271. this.drag.pinched = false;
  9272. this.pinch.scale = this._getScale();
  9273. };
  9274. /**
  9275. * handle drag start event
  9276. * @private
  9277. */
  9278. Graph.prototype._onDragStart = function () {
  9279. var drag = this.drag;
  9280. drag.selection = [];
  9281. drag.translation = this._getTranslation();
  9282. drag.nodeId = this._getNodeAt(drag.pointer);
  9283. // note: drag.pointer is set in _onTouch to get the initial touch location
  9284. var node = this.nodes[drag.nodeId];
  9285. if (node) {
  9286. // select the clicked node if not yet selected
  9287. if (!node.isSelected()) {
  9288. this._selectNodes([drag.nodeId]);
  9289. }
  9290. // create an array with the selected nodes and their original location and status
  9291. var me = this;
  9292. this.selection.forEach(function (id) {
  9293. var node = me.nodes[id];
  9294. if (node) {
  9295. var s = {
  9296. id: id,
  9297. node: node,
  9298. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  9299. x: node.x,
  9300. y: node.y,
  9301. xFixed: node.xFixed,
  9302. yFixed: node.yFixed
  9303. };
  9304. node.xFixed = true;
  9305. node.yFixed = true;
  9306. drag.selection.push(s);
  9307. }
  9308. });
  9309. }
  9310. };
  9311. /**
  9312. * handle drag event
  9313. * @private
  9314. */
  9315. Graph.prototype._onDrag = function (event) {
  9316. if (this.drag.pinched) {
  9317. return;
  9318. }
  9319. var pointer = this._getPointer(event.gesture.touches[0]);
  9320. var me = this,
  9321. drag = this.drag,
  9322. selection = drag.selection;
  9323. if (selection && selection.length) {
  9324. // calculate delta's and new location
  9325. var deltaX = pointer.x - drag.pointer.x,
  9326. deltaY = pointer.y - drag.pointer.y;
  9327. // update position of all selected nodes
  9328. selection.forEach(function (s) {
  9329. var node = s.node;
  9330. if (!s.xFixed) {
  9331. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  9332. }
  9333. if (!s.yFixed) {
  9334. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  9335. }
  9336. });
  9337. // start animation if not yet running
  9338. if (!this.moving) {
  9339. this.moving = true;
  9340. this.start();
  9341. }
  9342. }
  9343. else {
  9344. // move the graph
  9345. var diffX = pointer.x - this.drag.pointer.x;
  9346. var diffY = pointer.y - this.drag.pointer.y;
  9347. this._setTranslation(
  9348. this.drag.translation.x + diffX,
  9349. this.drag.translation.y + diffY);
  9350. this._redraw();
  9351. this.moved = true;
  9352. }
  9353. };
  9354. /**
  9355. * handle drag start event
  9356. * @private
  9357. */
  9358. Graph.prototype._onDragEnd = function () {
  9359. var selection = this.drag.selection;
  9360. if (selection) {
  9361. selection.forEach(function (s) {
  9362. // restore original xFixed and yFixed
  9363. s.node.xFixed = s.xFixed;
  9364. s.node.yFixed = s.yFixed;
  9365. });
  9366. }
  9367. };
  9368. /**
  9369. * handle tap/click event: select/unselect a node
  9370. * @private
  9371. */
  9372. Graph.prototype._onTap = function (event) {
  9373. var pointer = this._getPointer(event.gesture.touches[0]);
  9374. var nodeId = this._getNodeAt(pointer);
  9375. var node = this.nodes[nodeId];
  9376. if (node) {
  9377. // select this node
  9378. this._selectNodes([nodeId]);
  9379. if (!this.moving) {
  9380. this._redraw();
  9381. }
  9382. }
  9383. else {
  9384. // remove selection
  9385. this._unselectNodes();
  9386. this._redraw();
  9387. }
  9388. };
  9389. /**
  9390. * handle long tap event: multi select nodes
  9391. * @private
  9392. */
  9393. Graph.prototype._onHold = function (event) {
  9394. var pointer = this._getPointer(event.gesture.touches[0]);
  9395. var nodeId = this._getNodeAt(pointer);
  9396. var node = this.nodes[nodeId];
  9397. if (node) {
  9398. if (!node.isSelected()) {
  9399. // select this node, keep previous selection
  9400. var append = true;
  9401. this._selectNodes([nodeId], append);
  9402. }
  9403. else {
  9404. this._unselectNodes([nodeId]);
  9405. }
  9406. if (!this.moving) {
  9407. this._redraw();
  9408. }
  9409. }
  9410. else {
  9411. // Do nothing
  9412. }
  9413. };
  9414. /**
  9415. * Handle pinch event
  9416. * @param event
  9417. * @private
  9418. */
  9419. Graph.prototype._onPinch = function (event) {
  9420. var pointer = this._getPointer(event.gesture.center);
  9421. this.drag.pinched = true;
  9422. if (!('scale' in this.pinch)) {
  9423. this.pinch.scale = 1;
  9424. }
  9425. // TODO: enable moving while pinching?
  9426. var scale = this.pinch.scale * event.gesture.scale;
  9427. this._zoom(scale, pointer)
  9428. };
  9429. /**
  9430. * Zoom the graph in or out
  9431. * @param {Number} scale a number around 1, and between 0.01 and 10
  9432. * @param {{x: Number, y: Number}} pointer
  9433. * @return {Number} appliedScale scale is limited within the boundaries
  9434. * @private
  9435. */
  9436. Graph.prototype._zoom = function(scale, pointer) {
  9437. var scaleOld = this._getScale();
  9438. if (scale < 0.01) {
  9439. scale = 0.01;
  9440. }
  9441. if (scale > 10) {
  9442. scale = 10;
  9443. }
  9444. var translation = this._getTranslation();
  9445. var scaleFrac = scale / scaleOld;
  9446. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  9447. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  9448. this._setScale(scale);
  9449. this._setTranslation(tx, ty);
  9450. this._redraw();
  9451. return scale;
  9452. };
  9453. /**
  9454. * Event handler for mouse wheel event, used to zoom the timeline
  9455. * See http://adomas.org/javascript-mouse-wheel/
  9456. * https://github.com/EightMedia/hammer.js/issues/256
  9457. * @param {MouseEvent} event
  9458. * @private
  9459. */
  9460. Graph.prototype._onMouseWheel = function(event) {
  9461. // retrieve delta
  9462. var delta = 0;
  9463. if (event.wheelDelta) { /* IE/Opera. */
  9464. delta = event.wheelDelta/120;
  9465. } else if (event.detail) { /* Mozilla case. */
  9466. // In Mozilla, sign of delta is different than in IE.
  9467. // Also, delta is multiple of 3.
  9468. delta = -event.detail/3;
  9469. }
  9470. // If delta is nonzero, handle it.
  9471. // Basically, delta is now positive if wheel was scrolled up,
  9472. // and negative, if wheel was scrolled down.
  9473. if (delta) {
  9474. if (!('mouswheelScale' in this.pinch)) {
  9475. this.pinch.mouswheelScale = 1;
  9476. }
  9477. // calculate the new scale
  9478. var scale = this.pinch.mouswheelScale;
  9479. var zoom = delta / 10;
  9480. if (delta < 0) {
  9481. zoom = zoom / (1 - zoom);
  9482. }
  9483. scale *= (1 + zoom);
  9484. // calculate the pointer location
  9485. var gesture = util.fakeGesture(this, event);
  9486. var pointer = this._getPointer(gesture.center);
  9487. // apply the new scale
  9488. scale = this._zoom(scale, pointer);
  9489. // store the new, applied scale
  9490. this.pinch.mouswheelScale = scale;
  9491. }
  9492. // Prevent default actions caused by mouse wheel.
  9493. event.preventDefault();
  9494. };
  9495. /**
  9496. * Mouse move handler for checking whether the title moves over a node with a title.
  9497. * @param {Event} event
  9498. * @private
  9499. */
  9500. Graph.prototype._onMouseMoveTitle = function (event) {
  9501. var gesture = util.fakeGesture(this, event);
  9502. var pointer = this._getPointer(gesture.center);
  9503. // check if the previously selected node is still selected
  9504. if (this.popupNode) {
  9505. this._checkHidePopup(pointer);
  9506. }
  9507. // start a timeout that will check if the mouse is positioned above
  9508. // an element
  9509. var me = this;
  9510. var checkShow = function() {
  9511. me._checkShowPopup(pointer);
  9512. };
  9513. if (this.popupTimer) {
  9514. clearInterval(this.popupTimer); // stop any running timer
  9515. }
  9516. if (!this.leftButtonDown) {
  9517. this.popupTimer = setTimeout(checkShow, 300);
  9518. }
  9519. };
  9520. /**
  9521. * Check if there is an element on the given position in the graph
  9522. * (a node or edge). If so, and if this element has a title,
  9523. * show a popup window with its title.
  9524. *
  9525. * @param {{x:Number, y:Number}} pointer
  9526. * @private
  9527. */
  9528. Graph.prototype._checkShowPopup = function (pointer) {
  9529. var obj = {
  9530. left: this._canvasToX(pointer.x),
  9531. top: this._canvasToY(pointer.y),
  9532. right: this._canvasToX(pointer.x),
  9533. bottom: this._canvasToY(pointer.y)
  9534. };
  9535. var id;
  9536. var lastPopupNode = this.popupNode;
  9537. if (this.popupNode == undefined) {
  9538. // search the nodes for overlap, select the top one in case of multiple nodes
  9539. var nodes = this.nodes;
  9540. for (id in nodes) {
  9541. if (nodes.hasOwnProperty(id)) {
  9542. var node = nodes[id];
  9543. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  9544. this.popupNode = node;
  9545. break;
  9546. }
  9547. }
  9548. }
  9549. }
  9550. if (this.popupNode == undefined) {
  9551. // search the edges for overlap
  9552. var edges = this.edges;
  9553. for (id in edges) {
  9554. if (edges.hasOwnProperty(id)) {
  9555. var edge = edges[id];
  9556. if (edge.connected && (edge.getTitle() != undefined) &&
  9557. edge.isOverlappingWith(obj)) {
  9558. this.popupNode = edge;
  9559. break;
  9560. }
  9561. }
  9562. }
  9563. }
  9564. if (this.popupNode) {
  9565. // show popup message window
  9566. if (this.popupNode != lastPopupNode) {
  9567. var me = this;
  9568. if (!me.popup) {
  9569. me.popup = new Popup(me.frame);
  9570. }
  9571. // adjust a small offset such that the mouse cursor is located in the
  9572. // bottom left location of the popup, and you can easily move over the
  9573. // popup area
  9574. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  9575. me.popup.setText(me.popupNode.getTitle());
  9576. me.popup.show();
  9577. }
  9578. }
  9579. else {
  9580. if (this.popup) {
  9581. this.popup.hide();
  9582. }
  9583. }
  9584. };
  9585. /**
  9586. * Check if the popup must be hided, which is the case when the mouse is no
  9587. * longer hovering on the object
  9588. * @param {{x:Number, y:Number}} pointer
  9589. * @private
  9590. */
  9591. Graph.prototype._checkHidePopup = function (pointer) {
  9592. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  9593. this.popupNode = undefined;
  9594. if (this.popup) {
  9595. this.popup.hide();
  9596. }
  9597. }
  9598. };
  9599. /**
  9600. * Unselect selected nodes. If no selection array is provided, all nodes
  9601. * are unselected
  9602. * @param {Object[]} selection Array with selection objects, each selection
  9603. * object has a parameter row. Optional
  9604. * @param {Boolean} triggerSelect If true (default), the select event
  9605. * is triggered when nodes are unselected
  9606. * @return {Boolean} changed True if the selection is changed
  9607. * @private
  9608. */
  9609. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  9610. var changed = false;
  9611. var i, iMax, id;
  9612. if (selection) {
  9613. // remove provided selections
  9614. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9615. id = selection[i];
  9616. this.nodes[id].unselect();
  9617. var j = 0;
  9618. while (j < this.selection.length) {
  9619. if (this.selection[j] == id) {
  9620. this.selection.splice(j, 1);
  9621. changed = true;
  9622. }
  9623. else {
  9624. j++;
  9625. }
  9626. }
  9627. }
  9628. }
  9629. else if (this.selection && this.selection.length) {
  9630. // remove all selections
  9631. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9632. id = this.selection[i];
  9633. this.nodes[id].unselect();
  9634. changed = true;
  9635. }
  9636. this.selection = [];
  9637. }
  9638. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  9639. // fire the select event
  9640. this._trigger('select');
  9641. }
  9642. return changed;
  9643. };
  9644. /**
  9645. * select all nodes on given location x, y
  9646. * @param {Array} selection an array with node ids
  9647. * @param {boolean} append If true, the new selection will be appended to the
  9648. * current selection (except for duplicate entries)
  9649. * @return {Boolean} changed True if the selection is changed
  9650. * @private
  9651. */
  9652. Graph.prototype._selectNodes = function(selection, append) {
  9653. var changed = false;
  9654. var i, iMax;
  9655. // TODO: the selectNodes method is a little messy, rework this
  9656. // check if the current selection equals the desired selection
  9657. var selectionAlreadyThere = true;
  9658. if (selection.length != this.selection.length) {
  9659. selectionAlreadyThere = false;
  9660. }
  9661. else {
  9662. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  9663. if (selection[i] != this.selection[i]) {
  9664. selectionAlreadyThere = false;
  9665. break;
  9666. }
  9667. }
  9668. }
  9669. if (selectionAlreadyThere) {
  9670. return changed;
  9671. }
  9672. if (append == undefined || append == false) {
  9673. // first deselect any selected node
  9674. var triggerSelect = false;
  9675. changed = this._unselectNodes(undefined, triggerSelect);
  9676. }
  9677. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9678. // add each of the new selections, but only when they are not duplicate
  9679. var id = selection[i];
  9680. var isDuplicate = (this.selection.indexOf(id) != -1);
  9681. if (!isDuplicate) {
  9682. this.nodes[id].select();
  9683. this.selection.push(id);
  9684. changed = true;
  9685. }
  9686. }
  9687. if (changed) {
  9688. // fire the select event
  9689. this._trigger('select');
  9690. }
  9691. return changed;
  9692. };
  9693. /**
  9694. * retrieve all nodes overlapping with given object
  9695. * @param {Object} obj An object with parameters left, top, right, bottom
  9696. * @return {Number[]} An array with id's of the overlapping nodes
  9697. * @private
  9698. */
  9699. Graph.prototype._getNodesOverlappingWith = function (obj) {
  9700. var nodes = this.nodes,
  9701. overlappingNodes = [];
  9702. for (var id in nodes) {
  9703. if (nodes.hasOwnProperty(id)) {
  9704. if (nodes[id].isOverlappingWith(obj)) {
  9705. overlappingNodes.push(id);
  9706. }
  9707. }
  9708. }
  9709. return overlappingNodes;
  9710. };
  9711. /**
  9712. * retrieve the currently selected nodes
  9713. * @return {Number[] | String[]} selection An array with the ids of the
  9714. * selected nodes.
  9715. */
  9716. Graph.prototype.getSelection = function() {
  9717. return this.selection.concat([]);
  9718. };
  9719. /**
  9720. * select zero or more nodes
  9721. * @param {Number[] | String[]} selection An array with the ids of the
  9722. * selected nodes.
  9723. */
  9724. Graph.prototype.setSelection = function(selection) {
  9725. var i, iMax, id;
  9726. if (!selection || (selection.length == undefined))
  9727. throw 'Selection must be an array with ids';
  9728. // first unselect any selected node
  9729. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9730. id = this.selection[i];
  9731. this.nodes[id].unselect();
  9732. }
  9733. this.selection = [];
  9734. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9735. id = selection[i];
  9736. var node = this.nodes[id];
  9737. if (!node) {
  9738. throw new RangeError('Node with id "' + id + '" not found');
  9739. }
  9740. node.select();
  9741. this.selection.push(id);
  9742. }
  9743. this.redraw();
  9744. };
  9745. /**
  9746. * Validate the selection: remove ids of nodes which no longer exist
  9747. * @private
  9748. */
  9749. Graph.prototype._updateSelection = function () {
  9750. var i = 0;
  9751. while (i < this.selection.length) {
  9752. var id = this.selection[i];
  9753. if (!this.nodes[id]) {
  9754. this.selection.splice(i, 1);
  9755. }
  9756. else {
  9757. i++;
  9758. }
  9759. }
  9760. };
  9761. /**
  9762. * Temporary method to test calculating a hub value for the nodes
  9763. * @param {number} level Maximum number edges between two nodes in order
  9764. * to call them connected. Optional, 1 by default
  9765. * @return {Number[]} connectioncount array with the connection count
  9766. * for each node
  9767. * @private
  9768. */
  9769. Graph.prototype._getConnectionCount = function(level) {
  9770. if (level == undefined) {
  9771. level = 1;
  9772. }
  9773. // get the nodes connected to given nodes
  9774. function getConnectedNodes(nodes) {
  9775. var connectedNodes = [];
  9776. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  9777. var node = nodes[j];
  9778. // find all nodes connected to this node
  9779. var edges = node.edges;
  9780. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9781. var edge = edges[i];
  9782. var other = null;
  9783. // check if connected
  9784. if (edge.from == node)
  9785. other = edge.to;
  9786. else if (edge.to == node)
  9787. other = edge.from;
  9788. // check if the other node is not already in the list with nodes
  9789. var k, kMax;
  9790. if (other) {
  9791. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  9792. if (nodes[k] == other) {
  9793. other = null;
  9794. break;
  9795. }
  9796. }
  9797. }
  9798. if (other) {
  9799. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  9800. if (connectedNodes[k] == other) {
  9801. other = null;
  9802. break;
  9803. }
  9804. }
  9805. }
  9806. if (other)
  9807. connectedNodes.push(other);
  9808. }
  9809. }
  9810. return connectedNodes;
  9811. }
  9812. var connections = [];
  9813. var nodes = this.nodes;
  9814. for (var id in nodes) {
  9815. if (nodes.hasOwnProperty(id)) {
  9816. var c = [nodes[id]];
  9817. for (var l = 0; l < level; l++) {
  9818. c = c.concat(getConnectedNodes(c));
  9819. }
  9820. connections.push(c);
  9821. }
  9822. }
  9823. var hubs = [];
  9824. for (var i = 0, len = connections.length; i < len; i++) {
  9825. hubs.push(connections[i].length);
  9826. }
  9827. return hubs;
  9828. };
  9829. /**
  9830. * Set a new size for the graph
  9831. * @param {string} width Width in pixels or percentage (for example '800px'
  9832. * or '50%')
  9833. * @param {string} height Height in pixels or percentage (for example '400px'
  9834. * or '30%')
  9835. */
  9836. Graph.prototype.setSize = function(width, height) {
  9837. this.frame.style.width = width;
  9838. this.frame.style.height = height;
  9839. this.frame.canvas.style.width = '100%';
  9840. this.frame.canvas.style.height = '100%';
  9841. this.frame.canvas.width = this.frame.canvas.clientWidth;
  9842. this.frame.canvas.height = this.frame.canvas.clientHeight;
  9843. };
  9844. /**
  9845. * Set a data set with nodes for the graph
  9846. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  9847. * @private
  9848. */
  9849. Graph.prototype._setNodes = function(nodes) {
  9850. var oldNodesData = this.nodesData;
  9851. if (nodes instanceof DataSet || nodes instanceof DataView) {
  9852. this.nodesData = nodes;
  9853. }
  9854. else if (nodes instanceof Array) {
  9855. this.nodesData = new DataSet();
  9856. this.nodesData.add(nodes);
  9857. }
  9858. else if (!nodes) {
  9859. this.nodesData = new DataSet();
  9860. }
  9861. else {
  9862. throw new TypeError('Array or DataSet expected');
  9863. }
  9864. if (oldNodesData) {
  9865. // unsubscribe from old dataset
  9866. util.forEach(this.nodesListeners, function (callback, event) {
  9867. oldNodesData.unsubscribe(event, callback);
  9868. });
  9869. }
  9870. // remove drawn nodes
  9871. this.nodes = {};
  9872. if (this.nodesData) {
  9873. // subscribe to new dataset
  9874. var me = this;
  9875. util.forEach(this.nodesListeners, function (callback, event) {
  9876. me.nodesData.subscribe(event, callback);
  9877. });
  9878. // draw all new nodes
  9879. var ids = this.nodesData.getIds();
  9880. this._addNodes(ids);
  9881. }
  9882. this._updateSelection();
  9883. };
  9884. /**
  9885. * Add nodes
  9886. * @param {Number[] | String[]} ids
  9887. * @private
  9888. */
  9889. Graph.prototype._addNodes = function(ids) {
  9890. var id;
  9891. for (var i = 0, len = ids.length; i < len; i++) {
  9892. id = ids[i];
  9893. var data = this.nodesData.get(id);
  9894. var node = new Node(data, this.images, this.groups, this.constants);
  9895. this.nodes[id] = node; // note: this may replace an existing node
  9896. if (!node.isFixed()) {
  9897. // TODO: position new nodes in a smarter way!
  9898. var radius = this.constants.edges.length * 2;
  9899. var count = ids.length;
  9900. var angle = 2 * Math.PI * (i / count);
  9901. node.x = radius * Math.cos(angle);
  9902. node.y = radius * Math.sin(angle);
  9903. // note: no not use node.isMoving() here, as that gives the current
  9904. // velocity of the node, which is zero after creation of the node.
  9905. this.moving = true;
  9906. }
  9907. }
  9908. this._reconnectEdges();
  9909. this._updateValueRange(this.nodes);
  9910. };
  9911. /**
  9912. * Update existing nodes, or create them when not yet existing
  9913. * @param {Number[] | String[]} ids
  9914. * @private
  9915. */
  9916. Graph.prototype._updateNodes = function(ids) {
  9917. var nodes = this.nodes,
  9918. nodesData = this.nodesData;
  9919. for (var i = 0, len = ids.length; i < len; i++) {
  9920. var id = ids[i];
  9921. var node = nodes[id];
  9922. var data = nodesData.get(id);
  9923. if (node) {
  9924. // update node
  9925. node.setProperties(data, this.constants);
  9926. }
  9927. else {
  9928. // create node
  9929. node = new Node(properties, this.images, this.groups, this.constants);
  9930. nodes[id] = node;
  9931. if (!node.isFixed()) {
  9932. this.moving = true;
  9933. }
  9934. }
  9935. }
  9936. this._reconnectEdges();
  9937. this._updateValueRange(nodes);
  9938. };
  9939. /**
  9940. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  9941. * @param {Number[] | String[]} ids
  9942. * @private
  9943. */
  9944. Graph.prototype._removeNodes = function(ids) {
  9945. var nodes = this.nodes;
  9946. for (var i = 0, len = ids.length; i < len; i++) {
  9947. var id = ids[i];
  9948. delete nodes[id];
  9949. }
  9950. this._reconnectEdges();
  9951. this._updateSelection();
  9952. this._updateValueRange(nodes);
  9953. };
  9954. /**
  9955. * Load edges by reading the data table
  9956. * @param {Array | DataSet | DataView} edges The data containing the edges.
  9957. * @private
  9958. * @private
  9959. */
  9960. Graph.prototype._setEdges = function(edges) {
  9961. var oldEdgesData = this.edgesData;
  9962. if (edges instanceof DataSet || edges instanceof DataView) {
  9963. this.edgesData = edges;
  9964. }
  9965. else if (edges instanceof Array) {
  9966. this.edgesData = new DataSet();
  9967. this.edgesData.add(edges);
  9968. }
  9969. else if (!edges) {
  9970. this.edgesData = new DataSet();
  9971. }
  9972. else {
  9973. throw new TypeError('Array or DataSet expected');
  9974. }
  9975. if (oldEdgesData) {
  9976. // unsubscribe from old dataset
  9977. util.forEach(this.edgesListeners, function (callback, event) {
  9978. oldEdgesData.unsubscribe(event, callback);
  9979. });
  9980. }
  9981. // remove drawn edges
  9982. this.edges = {};
  9983. if (this.edgesData) {
  9984. // subscribe to new dataset
  9985. var me = this;
  9986. util.forEach(this.edgesListeners, function (callback, event) {
  9987. me.edgesData.subscribe(event, callback);
  9988. });
  9989. // draw all new nodes
  9990. var ids = this.edgesData.getIds();
  9991. this._addEdges(ids);
  9992. }
  9993. this._reconnectEdges();
  9994. };
  9995. /**
  9996. * Add edges
  9997. * @param {Number[] | String[]} ids
  9998. * @private
  9999. */
  10000. Graph.prototype._addEdges = function (ids) {
  10001. var edges = this.edges,
  10002. edgesData = this.edgesData;
  10003. for (var i = 0, len = ids.length; i < len; i++) {
  10004. var id = ids[i];
  10005. var oldEdge = edges[id];
  10006. if (oldEdge) {
  10007. oldEdge.disconnect();
  10008. }
  10009. var data = edgesData.get(id);
  10010. edges[id] = new Edge(data, this, this.constants);
  10011. }
  10012. this.moving = true;
  10013. this._updateValueRange(edges);
  10014. };
  10015. /**
  10016. * Update existing edges, or create them when not yet existing
  10017. * @param {Number[] | String[]} ids
  10018. * @private
  10019. */
  10020. Graph.prototype._updateEdges = function (ids) {
  10021. var edges = this.edges,
  10022. edgesData = this.edgesData;
  10023. for (var i = 0, len = ids.length; i < len; i++) {
  10024. var id = ids[i];
  10025. var data = edgesData.get(id);
  10026. var edge = edges[id];
  10027. if (edge) {
  10028. // update edge
  10029. edge.disconnect();
  10030. edge.setProperties(data, this.constants);
  10031. edge.connect();
  10032. }
  10033. else {
  10034. // create edge
  10035. edge = new Edge(data, this, this.constants);
  10036. this.edges[id] = edge;
  10037. }
  10038. }
  10039. this.moving = true;
  10040. this._updateValueRange(edges);
  10041. };
  10042. /**
  10043. * Remove existing edges. Non existing ids will be ignored
  10044. * @param {Number[] | String[]} ids
  10045. * @private
  10046. */
  10047. Graph.prototype._removeEdges = function (ids) {
  10048. var edges = this.edges;
  10049. for (var i = 0, len = ids.length; i < len; i++) {
  10050. var id = ids[i];
  10051. var edge = edges[id];
  10052. if (edge) {
  10053. edge.disconnect();
  10054. delete edges[id];
  10055. }
  10056. }
  10057. this.moving = true;
  10058. this._updateValueRange(edges);
  10059. };
  10060. /**
  10061. * Reconnect all edges
  10062. * @private
  10063. */
  10064. Graph.prototype._reconnectEdges = function() {
  10065. var id,
  10066. nodes = this.nodes,
  10067. edges = this.edges;
  10068. for (id in nodes) {
  10069. if (nodes.hasOwnProperty(id)) {
  10070. nodes[id].edges = [];
  10071. }
  10072. }
  10073. for (id in edges) {
  10074. if (edges.hasOwnProperty(id)) {
  10075. var edge = edges[id];
  10076. edge.from = null;
  10077. edge.to = null;
  10078. edge.connect();
  10079. }
  10080. }
  10081. };
  10082. /**
  10083. * Update the values of all object in the given array according to the current
  10084. * value range of the objects in the array.
  10085. * @param {Object} obj An object containing a set of Edges or Nodes
  10086. * The objects must have a method getValue() and
  10087. * setValueRange(min, max).
  10088. * @private
  10089. */
  10090. Graph.prototype._updateValueRange = function(obj) {
  10091. var id;
  10092. // determine the range of the objects
  10093. var valueMin = undefined;
  10094. var valueMax = undefined;
  10095. for (id in obj) {
  10096. if (obj.hasOwnProperty(id)) {
  10097. var value = obj[id].getValue();
  10098. if (value !== undefined) {
  10099. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  10100. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  10101. }
  10102. }
  10103. }
  10104. // adjust the range of all objects
  10105. if (valueMin !== undefined && valueMax !== undefined) {
  10106. for (id in obj) {
  10107. if (obj.hasOwnProperty(id)) {
  10108. obj[id].setValueRange(valueMin, valueMax);
  10109. }
  10110. }
  10111. }
  10112. };
  10113. /**
  10114. * Redraw the graph with the current data
  10115. * chart will be resized too.
  10116. */
  10117. Graph.prototype.redraw = function() {
  10118. this.setSize(this.width, this.height);
  10119. this._redraw();
  10120. };
  10121. /**
  10122. * Redraw the graph with the current data
  10123. * @private
  10124. */
  10125. Graph.prototype._redraw = function() {
  10126. var ctx = this.frame.canvas.getContext('2d');
  10127. // clear the canvas
  10128. var w = this.frame.canvas.width;
  10129. var h = this.frame.canvas.height;
  10130. ctx.clearRect(0, 0, w, h);
  10131. // set scaling and translation
  10132. ctx.save();
  10133. ctx.translate(this.translation.x, this.translation.y);
  10134. ctx.scale(this.scale, this.scale);
  10135. this._drawEdges(ctx);
  10136. this._drawNodes(ctx);
  10137. // restore original scaling and translation
  10138. ctx.restore();
  10139. };
  10140. /**
  10141. * Set the translation of the graph
  10142. * @param {Number} offsetX Horizontal offset
  10143. * @param {Number} offsetY Vertical offset
  10144. * @private
  10145. */
  10146. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  10147. if (this.translation === undefined) {
  10148. this.translation = {
  10149. x: 0,
  10150. y: 0
  10151. };
  10152. }
  10153. if (offsetX !== undefined) {
  10154. this.translation.x = offsetX;
  10155. }
  10156. if (offsetY !== undefined) {
  10157. this.translation.y = offsetY;
  10158. }
  10159. };
  10160. /**
  10161. * Get the translation of the graph
  10162. * @return {Object} translation An object with parameters x and y, both a number
  10163. * @private
  10164. */
  10165. Graph.prototype._getTranslation = function() {
  10166. return {
  10167. x: this.translation.x,
  10168. y: this.translation.y
  10169. };
  10170. };
  10171. /**
  10172. * Scale the graph
  10173. * @param {Number} scale Scaling factor 1.0 is unscaled
  10174. * @private
  10175. */
  10176. Graph.prototype._setScale = function(scale) {
  10177. this.scale = scale;
  10178. };
  10179. /**
  10180. * Get the current scale of the graph
  10181. * @return {Number} scale Scaling factor 1.0 is unscaled
  10182. * @private
  10183. */
  10184. Graph.prototype._getScale = function() {
  10185. return this.scale;
  10186. };
  10187. /**
  10188. * Convert a horizontal point on the HTML canvas to the x-value of the model
  10189. * @param {number} x
  10190. * @returns {number}
  10191. * @private
  10192. */
  10193. Graph.prototype._canvasToX = function(x) {
  10194. return (x - this.translation.x) / this.scale;
  10195. };
  10196. /**
  10197. * Convert an x-value in the model to a horizontal point on the HTML canvas
  10198. * @param {number} x
  10199. * @returns {number}
  10200. * @private
  10201. */
  10202. Graph.prototype._xToCanvas = function(x) {
  10203. return x * this.scale + this.translation.x;
  10204. };
  10205. /**
  10206. * Convert a vertical point on the HTML canvas to the y-value of the model
  10207. * @param {number} y
  10208. * @returns {number}
  10209. * @private
  10210. */
  10211. Graph.prototype._canvasToY = function(y) {
  10212. return (y - this.translation.y) / this.scale;
  10213. };
  10214. /**
  10215. * Convert an y-value in the model to a vertical point on the HTML canvas
  10216. * @param {number} y
  10217. * @returns {number}
  10218. * @private
  10219. */
  10220. Graph.prototype._yToCanvas = function(y) {
  10221. return y * this.scale + this.translation.y ;
  10222. };
  10223. /**
  10224. * Redraw all nodes
  10225. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  10226. * @param {CanvasRenderingContext2D} ctx
  10227. * @private
  10228. */
  10229. Graph.prototype._drawNodes = function(ctx) {
  10230. // first draw the unselected nodes
  10231. var nodes = this.nodes;
  10232. var selected = [];
  10233. for (var id in nodes) {
  10234. if (nodes.hasOwnProperty(id)) {
  10235. if (nodes[id].isSelected()) {
  10236. selected.push(id);
  10237. }
  10238. else {
  10239. nodes[id].draw(ctx);
  10240. }
  10241. }
  10242. }
  10243. // draw the selected nodes on top
  10244. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  10245. nodes[selected[s]].draw(ctx);
  10246. }
  10247. };
  10248. /**
  10249. * Redraw all edges
  10250. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  10251. * @param {CanvasRenderingContext2D} ctx
  10252. * @private
  10253. */
  10254. Graph.prototype._drawEdges = function(ctx) {
  10255. var edges = this.edges;
  10256. for (var id in edges) {
  10257. if (edges.hasOwnProperty(id)) {
  10258. var edge = edges[id];
  10259. if (edge.connected) {
  10260. edges[id].draw(ctx);
  10261. }
  10262. }
  10263. }
  10264. };
  10265. /**
  10266. * Find a stable position for all nodes
  10267. * @private
  10268. */
  10269. Graph.prototype._doStabilize = function() {
  10270. var start = new Date();
  10271. // find stable position
  10272. var count = 0;
  10273. var vmin = this.constants.minVelocity;
  10274. var stable = false;
  10275. while (!stable && count < this.constants.maxIterations) {
  10276. this._calculateForces();
  10277. this._discreteStepNodes();
  10278. stable = !this._isMoving(vmin);
  10279. count++;
  10280. }
  10281. var end = new Date();
  10282. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  10283. };
  10284. /**
  10285. * Calculate the external forces acting on the nodes
  10286. * Forces are caused by: edges, repulsing forces between nodes, gravity
  10287. * @private
  10288. */
  10289. Graph.prototype._calculateForces = function() {
  10290. // create a local edge to the nodes and edges, that is faster
  10291. var id, dx, dy, angle, distance, fx, fy,
  10292. repulsingForce, springForce, length, edgeLength,
  10293. nodes = this.nodes,
  10294. edges = this.edges;
  10295. // gravity, add a small constant force to pull the nodes towards the center of
  10296. // the graph
  10297. // Also, the forces are reset to zero in this loop by using _setForce instead
  10298. // of _addForce
  10299. var gravity = 0.01,
  10300. gx = this.frame.canvas.clientWidth / 2,
  10301. gy = this.frame.canvas.clientHeight / 2;
  10302. for (id in nodes) {
  10303. if (nodes.hasOwnProperty(id)) {
  10304. var node = nodes[id];
  10305. dx = gx - node.x;
  10306. dy = gy - node.y;
  10307. angle = Math.atan2(dy, dx);
  10308. fx = Math.cos(angle) * gravity;
  10309. fy = Math.sin(angle) * gravity;
  10310. node._setForce(fx, fy);
  10311. }
  10312. }
  10313. // repulsing forces between nodes
  10314. var minimumDistance = this.constants.nodes.distance,
  10315. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  10316. for (var id1 in nodes) {
  10317. if (nodes.hasOwnProperty(id1)) {
  10318. var node1 = nodes[id1];
  10319. for (var id2 in nodes) {
  10320. if (nodes.hasOwnProperty(id2)) {
  10321. var node2 = nodes[id2];
  10322. // calculate normally distributed force
  10323. dx = node2.x - node1.x;
  10324. dy = node2.y - node1.y;
  10325. distance = Math.sqrt(dx * dx + dy * dy);
  10326. angle = Math.atan2(dy, dx);
  10327. // TODO: correct factor for repulsing force
  10328. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10329. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10330. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  10331. fx = Math.cos(angle) * repulsingForce;
  10332. fy = Math.sin(angle) * repulsingForce;
  10333. node1._addForce(-fx, -fy);
  10334. node2._addForce(fx, fy);
  10335. }
  10336. }
  10337. }
  10338. }
  10339. /* TODO: re-implement repulsion of edges
  10340. for (var n = 0; n < nodes.length; n++) {
  10341. for (var l = 0; l < edges.length; l++) {
  10342. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  10343. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  10344. // calculate normally distributed force
  10345. dx = nodes[n].x - lx,
  10346. dy = nodes[n].y - ly,
  10347. distance = Math.sqrt(dx * dx + dy * dy),
  10348. angle = Math.atan2(dy, dx),
  10349. // TODO: correct factor for repulsing force
  10350. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10351. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  10352. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  10353. fx = Math.cos(angle) * repulsingforce,
  10354. fy = Math.sin(angle) * repulsingforce;
  10355. nodes[n]._addForce(fx, fy);
  10356. edges[l].from._addForce(-fx/2,-fy/2);
  10357. edges[l].to._addForce(-fx/2,-fy/2);
  10358. }
  10359. }
  10360. */
  10361. // forces caused by the edges, modelled as springs
  10362. for (id in edges) {
  10363. if (edges.hasOwnProperty(id)) {
  10364. var edge = edges[id];
  10365. if (edge.connected) {
  10366. dx = (edge.to.x - edge.from.x);
  10367. dy = (edge.to.y - edge.from.y);
  10368. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  10369. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  10370. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  10371. edgeLength = edge.length;
  10372. length = Math.sqrt(dx * dx + dy * dy);
  10373. angle = Math.atan2(dy, dx);
  10374. springForce = edge.stiffness * (edgeLength - length);
  10375. fx = Math.cos(angle) * springForce;
  10376. fy = Math.sin(angle) * springForce;
  10377. edge.from._addForce(-fx, -fy);
  10378. edge.to._addForce(fx, fy);
  10379. }
  10380. }
  10381. }
  10382. /* TODO: re-implement repulsion of edges
  10383. // repulsing forces between edges
  10384. var minimumDistance = this.constants.edges.distance,
  10385. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  10386. for (var l = 0; l < edges.length; l++) {
  10387. //Keep distance from other edge centers
  10388. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  10389. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  10390. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  10391. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  10392. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  10393. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  10394. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  10395. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  10396. // calculate normally distributed force
  10397. dx = l2x - lx,
  10398. dy = l2y - ly,
  10399. distance = Math.sqrt(dx * dx + dy * dy),
  10400. angle = Math.atan2(dy, dx),
  10401. // TODO: correct factor for repulsing force
  10402. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10403. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  10404. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  10405. fx = Math.cos(angle) * repulsingforce,
  10406. fy = Math.sin(angle) * repulsingforce;
  10407. edges[l].from._addForce(-fx, -fy);
  10408. edges[l].to._addForce(-fx, -fy);
  10409. edges[l2].from._addForce(fx, fy);
  10410. edges[l2].to._addForce(fx, fy);
  10411. }
  10412. }
  10413. */
  10414. };
  10415. /**
  10416. * Check if any of the nodes is still moving
  10417. * @param {number} vmin the minimum velocity considered as 'moving'
  10418. * @return {boolean} true if moving, false if non of the nodes is moving
  10419. * @private
  10420. */
  10421. Graph.prototype._isMoving = function(vmin) {
  10422. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  10423. var nodes = this.nodes;
  10424. for (var id in nodes) {
  10425. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  10426. return true;
  10427. }
  10428. }
  10429. return false;
  10430. };
  10431. /**
  10432. * Perform one discrete step for all nodes
  10433. * @private
  10434. */
  10435. Graph.prototype._discreteStepNodes = function() {
  10436. var interval = this.refreshRate / 1000.0; // in seconds
  10437. var nodes = this.nodes;
  10438. for (var id in nodes) {
  10439. if (nodes.hasOwnProperty(id)) {
  10440. nodes[id].discreteStep(interval);
  10441. }
  10442. }
  10443. };
  10444. /**
  10445. * Start animating nodes and edges
  10446. */
  10447. Graph.prototype.start = function() {
  10448. if (this.moving) {
  10449. this._calculateForces();
  10450. this._discreteStepNodes();
  10451. var vmin = this.constants.minVelocity;
  10452. this.moving = this._isMoving(vmin);
  10453. }
  10454. if (this.moving) {
  10455. // start animation. only start timer if it is not already running
  10456. if (!this.timer) {
  10457. var graph = this;
  10458. this.timer = window.setTimeout(function () {
  10459. graph.timer = undefined;
  10460. graph.start();
  10461. graph._redraw();
  10462. }, this.refreshRate);
  10463. }
  10464. }
  10465. else {
  10466. this._redraw();
  10467. }
  10468. };
  10469. /**
  10470. * Stop animating nodes and edges.
  10471. */
  10472. Graph.prototype.stop = function () {
  10473. if (this.timer) {
  10474. window.clearInterval(this.timer);
  10475. this.timer = undefined;
  10476. }
  10477. };
  10478. /**
  10479. * vis.js module exports
  10480. */
  10481. var vis = {
  10482. util: util,
  10483. events: events,
  10484. Controller: Controller,
  10485. DataSet: DataSet,
  10486. DataView: DataView,
  10487. Range: Range,
  10488. Stack: Stack,
  10489. TimeStep: TimeStep,
  10490. EventBus: EventBus,
  10491. components: {
  10492. items: {
  10493. Item: Item,
  10494. ItemBox: ItemBox,
  10495. ItemPoint: ItemPoint,
  10496. ItemRange: ItemRange
  10497. },
  10498. Component: Component,
  10499. Panel: Panel,
  10500. RootPanel: RootPanel,
  10501. ItemSet: ItemSet,
  10502. TimeAxis: TimeAxis
  10503. },
  10504. graph: {
  10505. Node: Node,
  10506. Edge: Edge,
  10507. Popup: Popup,
  10508. Groups: Groups,
  10509. Images: Images
  10510. },
  10511. Timeline: Timeline,
  10512. Graph: Graph
  10513. };
  10514. /**
  10515. * CommonJS module exports
  10516. */
  10517. if (typeof exports !== 'undefined') {
  10518. exports = vis;
  10519. }
  10520. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  10521. module.exports = vis;
  10522. }
  10523. /**
  10524. * AMD module exports
  10525. */
  10526. if (typeof(define) === 'function') {
  10527. define(function () {
  10528. return vis;
  10529. });
  10530. }
  10531. /**
  10532. * Window exports
  10533. */
  10534. if (typeof window !== 'undefined') {
  10535. // attach the module to the window, load as a regular javascript file
  10536. window['vis'] = vis;
  10537. }
  10538. },{"hammerjs":2,"moment":3}],2:[function(require,module,exports){
  10539. /*! Hammer.JS - v1.0.5 - 2013-04-07
  10540. * http://eightmedia.github.com/hammer.js
  10541. *
  10542. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  10543. * Licensed under the MIT license */
  10544. (function(window, undefined) {
  10545. 'use strict';
  10546. /**
  10547. * Hammer
  10548. * use this to create instances
  10549. * @param {HTMLElement} element
  10550. * @param {Object} options
  10551. * @returns {Hammer.Instance}
  10552. * @constructor
  10553. */
  10554. var Hammer = function(element, options) {
  10555. return new Hammer.Instance(element, options || {});
  10556. };
  10557. // default settings
  10558. Hammer.defaults = {
  10559. // add styles and attributes to the element to prevent the browser from doing
  10560. // its native behavior. this doesnt prevent the scrolling, but cancels
  10561. // the contextmenu, tap highlighting etc
  10562. // set to false to disable this
  10563. stop_browser_behavior: {
  10564. // this also triggers onselectstart=false for IE
  10565. userSelect: 'none',
  10566. // this makes the element blocking in IE10 >, you could experiment with the value
  10567. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  10568. touchAction: 'none',
  10569. touchCallout: 'none',
  10570. contentZooming: 'none',
  10571. userDrag: 'none',
  10572. tapHighlightColor: 'rgba(0,0,0,0)'
  10573. }
  10574. // more settings are defined per gesture at gestures.js
  10575. };
  10576. // detect touchevents
  10577. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  10578. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  10579. // dont use mouseevents on mobile devices
  10580. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  10581. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  10582. // eventtypes per touchevent (start, move, end)
  10583. // are filled by Hammer.event.determineEventTypes on setup
  10584. Hammer.EVENT_TYPES = {};
  10585. // direction defines
  10586. Hammer.DIRECTION_DOWN = 'down';
  10587. Hammer.DIRECTION_LEFT = 'left';
  10588. Hammer.DIRECTION_UP = 'up';
  10589. Hammer.DIRECTION_RIGHT = 'right';
  10590. // pointer type
  10591. Hammer.POINTER_MOUSE = 'mouse';
  10592. Hammer.POINTER_TOUCH = 'touch';
  10593. Hammer.POINTER_PEN = 'pen';
  10594. // touch event defines
  10595. Hammer.EVENT_START = 'start';
  10596. Hammer.EVENT_MOVE = 'move';
  10597. Hammer.EVENT_END = 'end';
  10598. // hammer document where the base events are added at
  10599. Hammer.DOCUMENT = document;
  10600. // plugins namespace
  10601. Hammer.plugins = {};
  10602. // if the window events are set...
  10603. Hammer.READY = false;
  10604. /**
  10605. * setup events to detect gestures on the document
  10606. */
  10607. function setup() {
  10608. if(Hammer.READY) {
  10609. return;
  10610. }
  10611. // find what eventtypes we add listeners to
  10612. Hammer.event.determineEventTypes();
  10613. // Register all gestures inside Hammer.gestures
  10614. for(var name in Hammer.gestures) {
  10615. if(Hammer.gestures.hasOwnProperty(name)) {
  10616. Hammer.detection.register(Hammer.gestures[name]);
  10617. }
  10618. }
  10619. // Add touch events on the document
  10620. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  10621. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  10622. // Hammer is ready...!
  10623. Hammer.READY = true;
  10624. }
  10625. /**
  10626. * create new hammer instance
  10627. * all methods should return the instance itself, so it is chainable.
  10628. * @param {HTMLElement} element
  10629. * @param {Object} [options={}]
  10630. * @returns {Hammer.Instance}
  10631. * @constructor
  10632. */
  10633. Hammer.Instance = function(element, options) {
  10634. var self = this;
  10635. // setup HammerJS window events and register all gestures
  10636. // this also sets up the default options
  10637. setup();
  10638. this.element = element;
  10639. // start/stop detection option
  10640. this.enabled = true;
  10641. // merge options
  10642. this.options = Hammer.utils.extend(
  10643. Hammer.utils.extend({}, Hammer.defaults),
  10644. options || {});
  10645. // add some css to the element to prevent the browser from doing its native behavoir
  10646. if(this.options.stop_browser_behavior) {
  10647. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  10648. }
  10649. // start detection on touchstart
  10650. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  10651. if(self.enabled) {
  10652. Hammer.detection.startDetect(self, ev);
  10653. }
  10654. });
  10655. // return instance
  10656. return this;
  10657. };
  10658. Hammer.Instance.prototype = {
  10659. /**
  10660. * bind events to the instance
  10661. * @param {String} gesture
  10662. * @param {Function} handler
  10663. * @returns {Hammer.Instance}
  10664. */
  10665. on: function onEvent(gesture, handler){
  10666. var gestures = gesture.split(' ');
  10667. for(var t=0; t<gestures.length; t++) {
  10668. this.element.addEventListener(gestures[t], handler, false);
  10669. }
  10670. return this;
  10671. },
  10672. /**
  10673. * unbind events to the instance
  10674. * @param {String} gesture
  10675. * @param {Function} handler
  10676. * @returns {Hammer.Instance}
  10677. */
  10678. off: function offEvent(gesture, handler){
  10679. var gestures = gesture.split(' ');
  10680. for(var t=0; t<gestures.length; t++) {
  10681. this.element.removeEventListener(gestures[t], handler, false);
  10682. }
  10683. return this;
  10684. },
  10685. /**
  10686. * trigger gesture event
  10687. * @param {String} gesture
  10688. * @param {Object} eventData
  10689. * @returns {Hammer.Instance}
  10690. */
  10691. trigger: function triggerEvent(gesture, eventData){
  10692. // create DOM event
  10693. var event = Hammer.DOCUMENT.createEvent('Event');
  10694. event.initEvent(gesture, true, true);
  10695. event.gesture = eventData;
  10696. // trigger on the target if it is in the instance element,
  10697. // this is for event delegation tricks
  10698. var element = this.element;
  10699. if(Hammer.utils.hasParent(eventData.target, element)) {
  10700. element = eventData.target;
  10701. }
  10702. element.dispatchEvent(event);
  10703. return this;
  10704. },
  10705. /**
  10706. * enable of disable hammer.js detection
  10707. * @param {Boolean} state
  10708. * @returns {Hammer.Instance}
  10709. */
  10710. enable: function enable(state) {
  10711. this.enabled = state;
  10712. return this;
  10713. }
  10714. };
  10715. /**
  10716. * this holds the last move event,
  10717. * used to fix empty touchend issue
  10718. * see the onTouch event for an explanation
  10719. * @type {Object}
  10720. */
  10721. var last_move_event = null;
  10722. /**
  10723. * when the mouse is hold down, this is true
  10724. * @type {Boolean}
  10725. */
  10726. var enable_detect = false;
  10727. /**
  10728. * when touch events have been fired, this is true
  10729. * @type {Boolean}
  10730. */
  10731. var touch_triggered = false;
  10732. Hammer.event = {
  10733. /**
  10734. * simple addEventListener
  10735. * @param {HTMLElement} element
  10736. * @param {String} type
  10737. * @param {Function} handler
  10738. */
  10739. bindDom: function(element, type, handler) {
  10740. var types = type.split(' ');
  10741. for(var t=0; t<types.length; t++) {
  10742. element.addEventListener(types[t], handler, false);
  10743. }
  10744. },
  10745. /**
  10746. * touch events with mouse fallback
  10747. * @param {HTMLElement} element
  10748. * @param {String} eventType like Hammer.EVENT_MOVE
  10749. * @param {Function} handler
  10750. */
  10751. onTouch: function onTouch(element, eventType, handler) {
  10752. var self = this;
  10753. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  10754. var sourceEventType = ev.type.toLowerCase();
  10755. // onmouseup, but when touchend has been fired we do nothing.
  10756. // this is for touchdevices which also fire a mouseup on touchend
  10757. if(sourceEventType.match(/mouse/) && touch_triggered) {
  10758. return;
  10759. }
  10760. // mousebutton must be down or a touch event
  10761. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  10762. sourceEventType.match(/pointerdown/) || // pointerevents touch
  10763. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  10764. ){
  10765. enable_detect = true;
  10766. }
  10767. // we are in a touch event, set the touch triggered bool to true,
  10768. // this for the conflicts that may occur on ios and android
  10769. if(sourceEventType.match(/touch|pointer/)) {
  10770. touch_triggered = true;
  10771. }
  10772. // count the total touches on the screen
  10773. var count_touches = 0;
  10774. // when touch has been triggered in this detection session
  10775. // and we are now handling a mouse event, we stop that to prevent conflicts
  10776. if(enable_detect) {
  10777. // update pointerevent
  10778. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  10779. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  10780. }
  10781. // touch
  10782. else if(sourceEventType.match(/touch/)) {
  10783. count_touches = ev.touches.length;
  10784. }
  10785. // mouse
  10786. else if(!touch_triggered) {
  10787. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  10788. }
  10789. // if we are in a end event, but when we remove one touch and
  10790. // we still have enough, set eventType to move
  10791. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  10792. eventType = Hammer.EVENT_MOVE;
  10793. }
  10794. // no touches, force the end event
  10795. else if(!count_touches) {
  10796. eventType = Hammer.EVENT_END;
  10797. }
  10798. // because touchend has no touches, and we often want to use these in our gestures,
  10799. // we send the last move event as our eventData in touchend
  10800. if(!count_touches && last_move_event !== null) {
  10801. ev = last_move_event;
  10802. }
  10803. // store the last move event
  10804. else {
  10805. last_move_event = ev;
  10806. }
  10807. // trigger the handler
  10808. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  10809. // remove pointerevent from list
  10810. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  10811. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  10812. }
  10813. }
  10814. //debug(sourceEventType +" "+ eventType);
  10815. // on the end we reset everything
  10816. if(!count_touches) {
  10817. last_move_event = null;
  10818. enable_detect = false;
  10819. touch_triggered = false;
  10820. Hammer.PointerEvent.reset();
  10821. }
  10822. });
  10823. },
  10824. /**
  10825. * we have different events for each device/browser
  10826. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  10827. */
  10828. determineEventTypes: function determineEventTypes() {
  10829. // determine the eventtype we want to set
  10830. var types;
  10831. // pointerEvents magic
  10832. if(Hammer.HAS_POINTEREVENTS) {
  10833. types = Hammer.PointerEvent.getEvents();
  10834. }
  10835. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  10836. else if(Hammer.NO_MOUSEEVENTS) {
  10837. types = [
  10838. 'touchstart',
  10839. 'touchmove',
  10840. 'touchend touchcancel'];
  10841. }
  10842. // for non pointer events browsers and mixed browsers,
  10843. // like chrome on windows8 touch laptop
  10844. else {
  10845. types = [
  10846. 'touchstart mousedown',
  10847. 'touchmove mousemove',
  10848. 'touchend touchcancel mouseup'];
  10849. }
  10850. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  10851. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  10852. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  10853. },
  10854. /**
  10855. * create touchlist depending on the event
  10856. * @param {Object} ev
  10857. * @param {String} eventType used by the fakemultitouch plugin
  10858. */
  10859. getTouchList: function getTouchList(ev/*, eventType*/) {
  10860. // get the fake pointerEvent touchlist
  10861. if(Hammer.HAS_POINTEREVENTS) {
  10862. return Hammer.PointerEvent.getTouchList();
  10863. }
  10864. // get the touchlist
  10865. else if(ev.touches) {
  10866. return ev.touches;
  10867. }
  10868. // make fake touchlist from mouse position
  10869. else {
  10870. return [{
  10871. identifier: 1,
  10872. pageX: ev.pageX,
  10873. pageY: ev.pageY,
  10874. target: ev.target
  10875. }];
  10876. }
  10877. },
  10878. /**
  10879. * collect event data for Hammer js
  10880. * @param {HTMLElement} element
  10881. * @param {String} eventType like Hammer.EVENT_MOVE
  10882. * @param {Object} eventData
  10883. */
  10884. collectEventData: function collectEventData(element, eventType, ev) {
  10885. var touches = this.getTouchList(ev, eventType);
  10886. // find out pointerType
  10887. var pointerType = Hammer.POINTER_TOUCH;
  10888. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  10889. pointerType = Hammer.POINTER_MOUSE;
  10890. }
  10891. return {
  10892. center : Hammer.utils.getCenter(touches),
  10893. timeStamp : new Date().getTime(),
  10894. target : ev.target,
  10895. touches : touches,
  10896. eventType : eventType,
  10897. pointerType : pointerType,
  10898. srcEvent : ev,
  10899. /**
  10900. * prevent the browser default actions
  10901. * mostly used to disable scrolling of the browser
  10902. */
  10903. preventDefault: function() {
  10904. if(this.srcEvent.preventManipulation) {
  10905. this.srcEvent.preventManipulation();
  10906. }
  10907. if(this.srcEvent.preventDefault) {
  10908. this.srcEvent.preventDefault();
  10909. }
  10910. },
  10911. /**
  10912. * stop bubbling the event up to its parents
  10913. */
  10914. stopPropagation: function() {
  10915. this.srcEvent.stopPropagation();
  10916. },
  10917. /**
  10918. * immediately stop gesture detection
  10919. * might be useful after a swipe was detected
  10920. * @return {*}
  10921. */
  10922. stopDetect: function() {
  10923. return Hammer.detection.stopDetect();
  10924. }
  10925. };
  10926. }
  10927. };
  10928. Hammer.PointerEvent = {
  10929. /**
  10930. * holds all pointers
  10931. * @type {Object}
  10932. */
  10933. pointers: {},
  10934. /**
  10935. * get a list of pointers
  10936. * @returns {Array} touchlist
  10937. */
  10938. getTouchList: function() {
  10939. var self = this;
  10940. var touchlist = [];
  10941. // we can use forEach since pointerEvents only is in IE10
  10942. Object.keys(self.pointers).sort().forEach(function(id) {
  10943. touchlist.push(self.pointers[id]);
  10944. });
  10945. return touchlist;
  10946. },
  10947. /**
  10948. * update the position of a pointer
  10949. * @param {String} type Hammer.EVENT_END
  10950. * @param {Object} pointerEvent
  10951. */
  10952. updatePointer: function(type, pointerEvent) {
  10953. if(type == Hammer.EVENT_END) {
  10954. this.pointers = {};
  10955. }
  10956. else {
  10957. pointerEvent.identifier = pointerEvent.pointerId;
  10958. this.pointers[pointerEvent.pointerId] = pointerEvent;
  10959. }
  10960. return Object.keys(this.pointers).length;
  10961. },
  10962. /**
  10963. * check if ev matches pointertype
  10964. * @param {String} pointerType Hammer.POINTER_MOUSE
  10965. * @param {PointerEvent} ev
  10966. */
  10967. matchType: function(pointerType, ev) {
  10968. if(!ev.pointerType) {
  10969. return false;
  10970. }
  10971. var types = {};
  10972. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  10973. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  10974. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  10975. return types[pointerType];
  10976. },
  10977. /**
  10978. * get events
  10979. */
  10980. getEvents: function() {
  10981. return [
  10982. 'pointerdown MSPointerDown',
  10983. 'pointermove MSPointerMove',
  10984. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  10985. ];
  10986. },
  10987. /**
  10988. * reset the list
  10989. */
  10990. reset: function() {
  10991. this.pointers = {};
  10992. }
  10993. };
  10994. Hammer.utils = {
  10995. /**
  10996. * extend method,
  10997. * also used for cloning when dest is an empty object
  10998. * @param {Object} dest
  10999. * @param {Object} src
  11000. * @parm {Boolean} merge do a merge
  11001. * @returns {Object} dest
  11002. */
  11003. extend: function extend(dest, src, merge) {
  11004. for (var key in src) {
  11005. if(dest[key] !== undefined && merge) {
  11006. continue;
  11007. }
  11008. dest[key] = src[key];
  11009. }
  11010. return dest;
  11011. },
  11012. /**
  11013. * find if a node is in the given parent
  11014. * used for event delegation tricks
  11015. * @param {HTMLElement} node
  11016. * @param {HTMLElement} parent
  11017. * @returns {boolean} has_parent
  11018. */
  11019. hasParent: function(node, parent) {
  11020. while(node){
  11021. if(node == parent) {
  11022. return true;
  11023. }
  11024. node = node.parentNode;
  11025. }
  11026. return false;
  11027. },
  11028. /**
  11029. * get the center of all the touches
  11030. * @param {Array} touches
  11031. * @returns {Object} center
  11032. */
  11033. getCenter: function getCenter(touches) {
  11034. var valuesX = [], valuesY = [];
  11035. for(var t= 0,len=touches.length; t<len; t++) {
  11036. valuesX.push(touches[t].pageX);
  11037. valuesY.push(touches[t].pageY);
  11038. }
  11039. return {
  11040. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  11041. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  11042. };
  11043. },
  11044. /**
  11045. * calculate the velocity between two points
  11046. * @param {Number} delta_time
  11047. * @param {Number} delta_x
  11048. * @param {Number} delta_y
  11049. * @returns {Object} velocity
  11050. */
  11051. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  11052. return {
  11053. x: Math.abs(delta_x / delta_time) || 0,
  11054. y: Math.abs(delta_y / delta_time) || 0
  11055. };
  11056. },
  11057. /**
  11058. * calculate the angle between two coordinates
  11059. * @param {Touch} touch1
  11060. * @param {Touch} touch2
  11061. * @returns {Number} angle
  11062. */
  11063. getAngle: function getAngle(touch1, touch2) {
  11064. var y = touch2.pageY - touch1.pageY,
  11065. x = touch2.pageX - touch1.pageX;
  11066. return Math.atan2(y, x) * 180 / Math.PI;
  11067. },
  11068. /**
  11069. * angle to direction define
  11070. * @param {Touch} touch1
  11071. * @param {Touch} touch2
  11072. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  11073. */
  11074. getDirection: function getDirection(touch1, touch2) {
  11075. var x = Math.abs(touch1.pageX - touch2.pageX),
  11076. y = Math.abs(touch1.pageY - touch2.pageY);
  11077. if(x >= y) {
  11078. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  11079. }
  11080. else {
  11081. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  11082. }
  11083. },
  11084. /**
  11085. * calculate the distance between two touches
  11086. * @param {Touch} touch1
  11087. * @param {Touch} touch2
  11088. * @returns {Number} distance
  11089. */
  11090. getDistance: function getDistance(touch1, touch2) {
  11091. var x = touch2.pageX - touch1.pageX,
  11092. y = touch2.pageY - touch1.pageY;
  11093. return Math.sqrt((x*x) + (y*y));
  11094. },
  11095. /**
  11096. * calculate the scale factor between two touchLists (fingers)
  11097. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  11098. * @param {Array} start
  11099. * @param {Array} end
  11100. * @returns {Number} scale
  11101. */
  11102. getScale: function getScale(start, end) {
  11103. // need two fingers...
  11104. if(start.length >= 2 && end.length >= 2) {
  11105. return this.getDistance(end[0], end[1]) /
  11106. this.getDistance(start[0], start[1]);
  11107. }
  11108. return 1;
  11109. },
  11110. /**
  11111. * calculate the rotation degrees between two touchLists (fingers)
  11112. * @param {Array} start
  11113. * @param {Array} end
  11114. * @returns {Number} rotation
  11115. */
  11116. getRotation: function getRotation(start, end) {
  11117. // need two fingers
  11118. if(start.length >= 2 && end.length >= 2) {
  11119. return this.getAngle(end[1], end[0]) -
  11120. this.getAngle(start[1], start[0]);
  11121. }
  11122. return 0;
  11123. },
  11124. /**
  11125. * boolean if the direction is vertical
  11126. * @param {String} direction
  11127. * @returns {Boolean} is_vertical
  11128. */
  11129. isVertical: function isVertical(direction) {
  11130. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  11131. },
  11132. /**
  11133. * stop browser default behavior with css props
  11134. * @param {HtmlElement} element
  11135. * @param {Object} css_props
  11136. */
  11137. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  11138. var prop,
  11139. vendors = ['webkit','khtml','moz','ms','o',''];
  11140. if(!css_props || !element.style) {
  11141. return;
  11142. }
  11143. // with css properties for modern browsers
  11144. for(var i = 0; i < vendors.length; i++) {
  11145. for(var p in css_props) {
  11146. if(css_props.hasOwnProperty(p)) {
  11147. prop = p;
  11148. // vender prefix at the property
  11149. if(vendors[i]) {
  11150. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  11151. }
  11152. // set the style
  11153. element.style[prop] = css_props[p];
  11154. }
  11155. }
  11156. }
  11157. // also the disable onselectstart
  11158. if(css_props.userSelect == 'none') {
  11159. element.onselectstart = function() {
  11160. return false;
  11161. };
  11162. }
  11163. }
  11164. };
  11165. Hammer.detection = {
  11166. // contains all registred Hammer.gestures in the correct order
  11167. gestures: [],
  11168. // data of the current Hammer.gesture detection session
  11169. current: null,
  11170. // the previous Hammer.gesture session data
  11171. // is a full clone of the previous gesture.current object
  11172. previous: null,
  11173. // when this becomes true, no gestures are fired
  11174. stopped: false,
  11175. /**
  11176. * start Hammer.gesture detection
  11177. * @param {Hammer.Instance} inst
  11178. * @param {Object} eventData
  11179. */
  11180. startDetect: function startDetect(inst, eventData) {
  11181. // already busy with a Hammer.gesture detection on an element
  11182. if(this.current) {
  11183. return;
  11184. }
  11185. this.stopped = false;
  11186. this.current = {
  11187. inst : inst, // reference to HammerInstance we're working for
  11188. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  11189. lastEvent : false, // last eventData
  11190. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  11191. };
  11192. this.detect(eventData);
  11193. },
  11194. /**
  11195. * Hammer.gesture detection
  11196. * @param {Object} eventData
  11197. * @param {Object} eventData
  11198. */
  11199. detect: function detect(eventData) {
  11200. if(!this.current || this.stopped) {
  11201. return;
  11202. }
  11203. // extend event data with calculations about scale, distance etc
  11204. eventData = this.extendEventData(eventData);
  11205. // instance options
  11206. var inst_options = this.current.inst.options;
  11207. // call Hammer.gesture handlers
  11208. for(var g=0,len=this.gestures.length; g<len; g++) {
  11209. var gesture = this.gestures[g];
  11210. // only when the instance options have enabled this gesture
  11211. if(!this.stopped && inst_options[gesture.name] !== false) {
  11212. // if a handler returns false, we stop with the detection
  11213. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  11214. this.stopDetect();
  11215. break;
  11216. }
  11217. }
  11218. }
  11219. // store as previous event event
  11220. if(this.current) {
  11221. this.current.lastEvent = eventData;
  11222. }
  11223. // endevent, but not the last touch, so dont stop
  11224. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  11225. this.stopDetect();
  11226. }
  11227. return eventData;
  11228. },
  11229. /**
  11230. * clear the Hammer.gesture vars
  11231. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  11232. * to stop other Hammer.gestures from being fired
  11233. */
  11234. stopDetect: function stopDetect() {
  11235. // clone current data to the store as the previous gesture
  11236. // used for the double tap gesture, since this is an other gesture detect session
  11237. this.previous = Hammer.utils.extend({}, this.current);
  11238. // reset the current
  11239. this.current = null;
  11240. // stopped!
  11241. this.stopped = true;
  11242. },
  11243. /**
  11244. * extend eventData for Hammer.gestures
  11245. * @param {Object} ev
  11246. * @returns {Object} ev
  11247. */
  11248. extendEventData: function extendEventData(ev) {
  11249. var startEv = this.current.startEvent;
  11250. // if the touches change, set the new touches over the startEvent touches
  11251. // this because touchevents don't have all the touches on touchstart, or the
  11252. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  11253. // but, sometimes it happens that both fingers are touching at the EXACT same time
  11254. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  11255. // extend 1 level deep to get the touchlist with the touch objects
  11256. startEv.touches = [];
  11257. for(var i=0,len=ev.touches.length; i<len; i++) {
  11258. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  11259. }
  11260. }
  11261. var delta_time = ev.timeStamp - startEv.timeStamp,
  11262. delta_x = ev.center.pageX - startEv.center.pageX,
  11263. delta_y = ev.center.pageY - startEv.center.pageY,
  11264. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  11265. Hammer.utils.extend(ev, {
  11266. deltaTime : delta_time,
  11267. deltaX : delta_x,
  11268. deltaY : delta_y,
  11269. velocityX : velocity.x,
  11270. velocityY : velocity.y,
  11271. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  11272. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  11273. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  11274. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  11275. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  11276. startEvent : startEv
  11277. });
  11278. return ev;
  11279. },
  11280. /**
  11281. * register new gesture
  11282. * @param {Object} gesture object, see gestures.js for documentation
  11283. * @returns {Array} gestures
  11284. */
  11285. register: function register(gesture) {
  11286. // add an enable gesture options if there is no given
  11287. var options = gesture.defaults || {};
  11288. if(options[gesture.name] === undefined) {
  11289. options[gesture.name] = true;
  11290. }
  11291. // extend Hammer default options with the Hammer.gesture options
  11292. Hammer.utils.extend(Hammer.defaults, options, true);
  11293. // set its index
  11294. gesture.index = gesture.index || 1000;
  11295. // add Hammer.gesture to the list
  11296. this.gestures.push(gesture);
  11297. // sort the list by index
  11298. this.gestures.sort(function(a, b) {
  11299. if (a.index < b.index) {
  11300. return -1;
  11301. }
  11302. if (a.index > b.index) {
  11303. return 1;
  11304. }
  11305. return 0;
  11306. });
  11307. return this.gestures;
  11308. }
  11309. };
  11310. Hammer.gestures = Hammer.gestures || {};
  11311. /**
  11312. * Custom gestures
  11313. * ==============================
  11314. *
  11315. * Gesture object
  11316. * --------------------
  11317. * The object structure of a gesture:
  11318. *
  11319. * { name: 'mygesture',
  11320. * index: 1337,
  11321. * defaults: {
  11322. * mygesture_option: true
  11323. * }
  11324. * handler: function(type, ev, inst) {
  11325. * // trigger gesture event
  11326. * inst.trigger(this.name, ev);
  11327. * }
  11328. * }
  11329. * @param {String} name
  11330. * this should be the name of the gesture, lowercase
  11331. * it is also being used to disable/enable the gesture per instance config.
  11332. *
  11333. * @param {Number} [index=1000]
  11334. * the index of the gesture, where it is going to be in the stack of gestures detection
  11335. * like when you build an gesture that depends on the drag gesture, it is a good
  11336. * idea to place it after the index of the drag gesture.
  11337. *
  11338. * @param {Object} [defaults={}]
  11339. * the default settings of the gesture. these are added to the instance settings,
  11340. * and can be overruled per instance. you can also add the name of the gesture,
  11341. * but this is also added by default (and set to true).
  11342. *
  11343. * @param {Function} handler
  11344. * this handles the gesture detection of your custom gesture and receives the
  11345. * following arguments:
  11346. *
  11347. * @param {Object} eventData
  11348. * event data containing the following properties:
  11349. * timeStamp {Number} time the event occurred
  11350. * target {HTMLElement} target element
  11351. * touches {Array} touches (fingers, pointers, mouse) on the screen
  11352. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  11353. * center {Object} center position of the touches. contains pageX and pageY
  11354. * deltaTime {Number} the total time of the touches in the screen
  11355. * deltaX {Number} the delta on x axis we haved moved
  11356. * deltaY {Number} the delta on y axis we haved moved
  11357. * velocityX {Number} the velocity on the x
  11358. * velocityY {Number} the velocity on y
  11359. * angle {Number} the angle we are moving
  11360. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  11361. * distance {Number} the distance we haved moved
  11362. * scale {Number} scaling of the touches, needs 2 touches
  11363. * rotation {Number} rotation of the touches, needs 2 touches *
  11364. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  11365. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  11366. * startEvent {Object} contains the same properties as above,
  11367. * but from the first touch. this is used to calculate
  11368. * distances, deltaTime, scaling etc
  11369. *
  11370. * @param {Hammer.Instance} inst
  11371. * the instance we are doing the detection for. you can get the options from
  11372. * the inst.options object and trigger the gesture event by calling inst.trigger
  11373. *
  11374. *
  11375. * Handle gestures
  11376. * --------------------
  11377. * inside the handler you can get/set Hammer.detection.current. This is the current
  11378. * detection session. It has the following properties
  11379. * @param {String} name
  11380. * contains the name of the gesture we have detected. it has not a real function,
  11381. * only to check in other gestures if something is detected.
  11382. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  11383. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  11384. *
  11385. * @readonly
  11386. * @param {Hammer.Instance} inst
  11387. * the instance we do the detection for
  11388. *
  11389. * @readonly
  11390. * @param {Object} startEvent
  11391. * contains the properties of the first gesture detection in this session.
  11392. * Used for calculations about timing, distance, etc.
  11393. *
  11394. * @readonly
  11395. * @param {Object} lastEvent
  11396. * contains all the properties of the last gesture detect in this session.
  11397. *
  11398. * after the gesture detection session has been completed (user has released the screen)
  11399. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  11400. * this is usefull for gestures like doubletap, where you need to know if the
  11401. * previous gesture was a tap
  11402. *
  11403. * options that have been set by the instance can be received by calling inst.options
  11404. *
  11405. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  11406. * The first param is the name of your gesture, the second the event argument
  11407. *
  11408. *
  11409. * Register gestures
  11410. * --------------------
  11411. * When an gesture is added to the Hammer.gestures object, it is auto registered
  11412. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  11413. * manually and pass your gesture object as a param
  11414. *
  11415. */
  11416. /**
  11417. * Hold
  11418. * Touch stays at the same place for x time
  11419. * @events hold
  11420. */
  11421. Hammer.gestures.Hold = {
  11422. name: 'hold',
  11423. index: 10,
  11424. defaults: {
  11425. hold_timeout : 500,
  11426. hold_threshold : 1
  11427. },
  11428. timer: null,
  11429. handler: function holdGesture(ev, inst) {
  11430. switch(ev.eventType) {
  11431. case Hammer.EVENT_START:
  11432. // clear any running timers
  11433. clearTimeout(this.timer);
  11434. // set the gesture so we can check in the timeout if it still is
  11435. Hammer.detection.current.name = this.name;
  11436. // set timer and if after the timeout it still is hold,
  11437. // we trigger the hold event
  11438. this.timer = setTimeout(function() {
  11439. if(Hammer.detection.current.name == 'hold') {
  11440. inst.trigger('hold', ev);
  11441. }
  11442. }, inst.options.hold_timeout);
  11443. break;
  11444. // when you move or end we clear the timer
  11445. case Hammer.EVENT_MOVE:
  11446. if(ev.distance > inst.options.hold_threshold) {
  11447. clearTimeout(this.timer);
  11448. }
  11449. break;
  11450. case Hammer.EVENT_END:
  11451. clearTimeout(this.timer);
  11452. break;
  11453. }
  11454. }
  11455. };
  11456. /**
  11457. * Tap/DoubleTap
  11458. * Quick touch at a place or double at the same place
  11459. * @events tap, doubletap
  11460. */
  11461. Hammer.gestures.Tap = {
  11462. name: 'tap',
  11463. index: 100,
  11464. defaults: {
  11465. tap_max_touchtime : 250,
  11466. tap_max_distance : 10,
  11467. tap_always : true,
  11468. doubletap_distance : 20,
  11469. doubletap_interval : 300
  11470. },
  11471. handler: function tapGesture(ev, inst) {
  11472. if(ev.eventType == Hammer.EVENT_END) {
  11473. // previous gesture, for the double tap since these are two different gesture detections
  11474. var prev = Hammer.detection.previous,
  11475. did_doubletap = false;
  11476. // when the touchtime is higher then the max touch time
  11477. // or when the moving distance is too much
  11478. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  11479. ev.distance > inst.options.tap_max_distance) {
  11480. return;
  11481. }
  11482. // check if double tap
  11483. if(prev && prev.name == 'tap' &&
  11484. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  11485. ev.distance < inst.options.doubletap_distance) {
  11486. inst.trigger('doubletap', ev);
  11487. did_doubletap = true;
  11488. }
  11489. // do a single tap
  11490. if(!did_doubletap || inst.options.tap_always) {
  11491. Hammer.detection.current.name = 'tap';
  11492. inst.trigger(Hammer.detection.current.name, ev);
  11493. }
  11494. }
  11495. }
  11496. };
  11497. /**
  11498. * Swipe
  11499. * triggers swipe events when the end velocity is above the threshold
  11500. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  11501. */
  11502. Hammer.gestures.Swipe = {
  11503. name: 'swipe',
  11504. index: 40,
  11505. defaults: {
  11506. // set 0 for unlimited, but this can conflict with transform
  11507. swipe_max_touches : 1,
  11508. swipe_velocity : 0.7
  11509. },
  11510. handler: function swipeGesture(ev, inst) {
  11511. if(ev.eventType == Hammer.EVENT_END) {
  11512. // max touches
  11513. if(inst.options.swipe_max_touches > 0 &&
  11514. ev.touches.length > inst.options.swipe_max_touches) {
  11515. return;
  11516. }
  11517. // when the distance we moved is too small we skip this gesture
  11518. // or we can be already in dragging
  11519. if(ev.velocityX > inst.options.swipe_velocity ||
  11520. ev.velocityY > inst.options.swipe_velocity) {
  11521. // trigger swipe events
  11522. inst.trigger(this.name, ev);
  11523. inst.trigger(this.name + ev.direction, ev);
  11524. }
  11525. }
  11526. }
  11527. };
  11528. /**
  11529. * Drag
  11530. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  11531. * moving left and right is a good practice. When all the drag events are blocking
  11532. * you disable scrolling on that area.
  11533. * @events drag, drapleft, dragright, dragup, dragdown
  11534. */
  11535. Hammer.gestures.Drag = {
  11536. name: 'drag',
  11537. index: 50,
  11538. defaults: {
  11539. drag_min_distance : 10,
  11540. // set 0 for unlimited, but this can conflict with transform
  11541. drag_max_touches : 1,
  11542. // prevent default browser behavior when dragging occurs
  11543. // be careful with it, it makes the element a blocking element
  11544. // when you are using the drag gesture, it is a good practice to set this true
  11545. drag_block_horizontal : false,
  11546. drag_block_vertical : false,
  11547. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  11548. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  11549. drag_lock_to_axis : false,
  11550. // drag lock only kicks in when distance > drag_lock_min_distance
  11551. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  11552. drag_lock_min_distance : 25
  11553. },
  11554. triggered: false,
  11555. handler: function dragGesture(ev, inst) {
  11556. // current gesture isnt drag, but dragged is true
  11557. // this means an other gesture is busy. now call dragend
  11558. if(Hammer.detection.current.name != this.name && this.triggered) {
  11559. inst.trigger(this.name +'end', ev);
  11560. this.triggered = false;
  11561. return;
  11562. }
  11563. // max touches
  11564. if(inst.options.drag_max_touches > 0 &&
  11565. ev.touches.length > inst.options.drag_max_touches) {
  11566. return;
  11567. }
  11568. switch(ev.eventType) {
  11569. case Hammer.EVENT_START:
  11570. this.triggered = false;
  11571. break;
  11572. case Hammer.EVENT_MOVE:
  11573. // when the distance we moved is too small we skip this gesture
  11574. // or we can be already in dragging
  11575. if(ev.distance < inst.options.drag_min_distance &&
  11576. Hammer.detection.current.name != this.name) {
  11577. return;
  11578. }
  11579. // we are dragging!
  11580. Hammer.detection.current.name = this.name;
  11581. // lock drag to axis?
  11582. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  11583. ev.drag_locked_to_axis = true;
  11584. }
  11585. var last_direction = Hammer.detection.current.lastEvent.direction;
  11586. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  11587. // keep direction on the axis that the drag gesture started on
  11588. if(Hammer.utils.isVertical(last_direction)) {
  11589. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  11590. }
  11591. else {
  11592. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  11593. }
  11594. }
  11595. // first time, trigger dragstart event
  11596. if(!this.triggered) {
  11597. inst.trigger(this.name +'start', ev);
  11598. this.triggered = true;
  11599. }
  11600. // trigger normal event
  11601. inst.trigger(this.name, ev);
  11602. // direction event, like dragdown
  11603. inst.trigger(this.name + ev.direction, ev);
  11604. // block the browser events
  11605. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  11606. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  11607. ev.preventDefault();
  11608. }
  11609. break;
  11610. case Hammer.EVENT_END:
  11611. // trigger dragend
  11612. if(this.triggered) {
  11613. inst.trigger(this.name +'end', ev);
  11614. }
  11615. this.triggered = false;
  11616. break;
  11617. }
  11618. }
  11619. };
  11620. /**
  11621. * Transform
  11622. * User want to scale or rotate with 2 fingers
  11623. * @events transform, pinch, pinchin, pinchout, rotate
  11624. */
  11625. Hammer.gestures.Transform = {
  11626. name: 'transform',
  11627. index: 45,
  11628. defaults: {
  11629. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  11630. transform_min_scale : 0.01,
  11631. // rotation in degrees
  11632. transform_min_rotation : 1,
  11633. // prevent default browser behavior when two touches are on the screen
  11634. // but it makes the element a blocking element
  11635. // when you are using the transform gesture, it is a good practice to set this true
  11636. transform_always_block : false
  11637. },
  11638. triggered: false,
  11639. handler: function transformGesture(ev, inst) {
  11640. // current gesture isnt drag, but dragged is true
  11641. // this means an other gesture is busy. now call dragend
  11642. if(Hammer.detection.current.name != this.name && this.triggered) {
  11643. inst.trigger(this.name +'end', ev);
  11644. this.triggered = false;
  11645. return;
  11646. }
  11647. // atleast multitouch
  11648. if(ev.touches.length < 2) {
  11649. return;
  11650. }
  11651. // prevent default when two fingers are on the screen
  11652. if(inst.options.transform_always_block) {
  11653. ev.preventDefault();
  11654. }
  11655. switch(ev.eventType) {
  11656. case Hammer.EVENT_START:
  11657. this.triggered = false;
  11658. break;
  11659. case Hammer.EVENT_MOVE:
  11660. var scale_threshold = Math.abs(1-ev.scale);
  11661. var rotation_threshold = Math.abs(ev.rotation);
  11662. // when the distance we moved is too small we skip this gesture
  11663. // or we can be already in dragging
  11664. if(scale_threshold < inst.options.transform_min_scale &&
  11665. rotation_threshold < inst.options.transform_min_rotation) {
  11666. return;
  11667. }
  11668. // we are transforming!
  11669. Hammer.detection.current.name = this.name;
  11670. // first time, trigger dragstart event
  11671. if(!this.triggered) {
  11672. inst.trigger(this.name +'start', ev);
  11673. this.triggered = true;
  11674. }
  11675. inst.trigger(this.name, ev); // basic transform event
  11676. // trigger rotate event
  11677. if(rotation_threshold > inst.options.transform_min_rotation) {
  11678. inst.trigger('rotate', ev);
  11679. }
  11680. // trigger pinch event
  11681. if(scale_threshold > inst.options.transform_min_scale) {
  11682. inst.trigger('pinch', ev);
  11683. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  11684. }
  11685. break;
  11686. case Hammer.EVENT_END:
  11687. // trigger dragend
  11688. if(this.triggered) {
  11689. inst.trigger(this.name +'end', ev);
  11690. }
  11691. this.triggered = false;
  11692. break;
  11693. }
  11694. }
  11695. };
  11696. /**
  11697. * Touch
  11698. * Called as first, tells the user has touched the screen
  11699. * @events touch
  11700. */
  11701. Hammer.gestures.Touch = {
  11702. name: 'touch',
  11703. index: -Infinity,
  11704. defaults: {
  11705. // call preventDefault at touchstart, and makes the element blocking by
  11706. // disabling the scrolling of the page, but it improves gestures like
  11707. // transforming and dragging.
  11708. // be careful with using this, it can be very annoying for users to be stuck
  11709. // on the page
  11710. prevent_default: false,
  11711. // disable mouse events, so only touch (or pen!) input triggers events
  11712. prevent_mouseevents: false
  11713. },
  11714. handler: function touchGesture(ev, inst) {
  11715. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  11716. ev.stopDetect();
  11717. return;
  11718. }
  11719. if(inst.options.prevent_default) {
  11720. ev.preventDefault();
  11721. }
  11722. if(ev.eventType == Hammer.EVENT_START) {
  11723. inst.trigger(this.name, ev);
  11724. }
  11725. }
  11726. };
  11727. /**
  11728. * Release
  11729. * Called as last, tells the user has released the screen
  11730. * @events release
  11731. */
  11732. Hammer.gestures.Release = {
  11733. name: 'release',
  11734. index: Infinity,
  11735. handler: function releaseGesture(ev, inst) {
  11736. if(ev.eventType == Hammer.EVENT_END) {
  11737. inst.trigger(this.name, ev);
  11738. }
  11739. }
  11740. };
  11741. // node export
  11742. if(typeof module === 'object' && typeof module.exports === 'object'){
  11743. module.exports = Hammer;
  11744. }
  11745. // just window export
  11746. else {
  11747. window.Hammer = Hammer;
  11748. // requireJS module definition
  11749. if(typeof window.define === 'function' && window.define.amd) {
  11750. window.define('hammer', [], function() {
  11751. return Hammer;
  11752. });
  11753. }
  11754. }
  11755. })(this);
  11756. },{}],3:[function(require,module,exports){
  11757. //! moment.js
  11758. //! version : 2.5.0
  11759. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  11760. //! license : MIT
  11761. //! momentjs.com
  11762. (function (undefined) {
  11763. /************************************
  11764. Constants
  11765. ************************************/
  11766. var moment,
  11767. VERSION = "2.5.0",
  11768. global = this,
  11769. round = Math.round,
  11770. i,
  11771. YEAR = 0,
  11772. MONTH = 1,
  11773. DATE = 2,
  11774. HOUR = 3,
  11775. MINUTE = 4,
  11776. SECOND = 5,
  11777. MILLISECOND = 6,
  11778. // internal storage for language config files
  11779. languages = {},
  11780. // check for nodeJS
  11781. hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
  11782. // ASP.NET json date format regex
  11783. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  11784. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  11785. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  11786. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  11787. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  11788. // format tokens
  11789. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  11790. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  11791. // parsing token regexes
  11792. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  11793. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  11794. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  11795. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  11796. parseTokenDigits = /\d+/, // nonzero number of digits
  11797. 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.
  11798. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  11799. parseTokenT = /T/i, // T (ISO separator)
  11800. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  11801. //strict parsing regexes
  11802. parseTokenOneDigit = /\d/, // 0 - 9
  11803. parseTokenTwoDigits = /\d\d/, // 00 - 99
  11804. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  11805. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  11806. parseTokenSixDigits = /[+\-]?\d{6}/, // -999,999 - 999,999
  11807. // iso 8601 regex
  11808. // 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 or +00)
  11809. 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)?|\s*Z)?)?$/,
  11810. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  11811. isoDates = [
  11812. 'YYYY-MM-DD',
  11813. 'GGGG-[W]WW',
  11814. 'GGGG-[W]WW-E',
  11815. 'YYYY-DDD'
  11816. ],
  11817. // iso time formats and regexes
  11818. isoTimes = [
  11819. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  11820. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  11821. ['HH:mm', /(T| )\d\d:\d\d/],
  11822. ['HH', /(T| )\d\d/]
  11823. ],
  11824. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  11825. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  11826. // getter and setter names
  11827. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  11828. unitMillisecondFactors = {
  11829. 'Milliseconds' : 1,
  11830. 'Seconds' : 1e3,
  11831. 'Minutes' : 6e4,
  11832. 'Hours' : 36e5,
  11833. 'Days' : 864e5,
  11834. 'Months' : 2592e6,
  11835. 'Years' : 31536e6
  11836. },
  11837. unitAliases = {
  11838. ms : 'millisecond',
  11839. s : 'second',
  11840. m : 'minute',
  11841. h : 'hour',
  11842. d : 'day',
  11843. D : 'date',
  11844. w : 'week',
  11845. W : 'isoWeek',
  11846. M : 'month',
  11847. y : 'year',
  11848. DDD : 'dayOfYear',
  11849. e : 'weekday',
  11850. E : 'isoWeekday',
  11851. gg: 'weekYear',
  11852. GG: 'isoWeekYear'
  11853. },
  11854. camelFunctions = {
  11855. dayofyear : 'dayOfYear',
  11856. isoweekday : 'isoWeekday',
  11857. isoweek : 'isoWeek',
  11858. weekyear : 'weekYear',
  11859. isoweekyear : 'isoWeekYear'
  11860. },
  11861. // format function strings
  11862. formatFunctions = {},
  11863. // tokens to ordinalize and pad
  11864. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  11865. paddedTokens = 'M D H h m s w W'.split(' '),
  11866. formatTokenFunctions = {
  11867. M : function () {
  11868. return this.month() + 1;
  11869. },
  11870. MMM : function (format) {
  11871. return this.lang().monthsShort(this, format);
  11872. },
  11873. MMMM : function (format) {
  11874. return this.lang().months(this, format);
  11875. },
  11876. D : function () {
  11877. return this.date();
  11878. },
  11879. DDD : function () {
  11880. return this.dayOfYear();
  11881. },
  11882. d : function () {
  11883. return this.day();
  11884. },
  11885. dd : function (format) {
  11886. return this.lang().weekdaysMin(this, format);
  11887. },
  11888. ddd : function (format) {
  11889. return this.lang().weekdaysShort(this, format);
  11890. },
  11891. dddd : function (format) {
  11892. return this.lang().weekdays(this, format);
  11893. },
  11894. w : function () {
  11895. return this.week();
  11896. },
  11897. W : function () {
  11898. return this.isoWeek();
  11899. },
  11900. YY : function () {
  11901. return leftZeroFill(this.year() % 100, 2);
  11902. },
  11903. YYYY : function () {
  11904. return leftZeroFill(this.year(), 4);
  11905. },
  11906. YYYYY : function () {
  11907. return leftZeroFill(this.year(), 5);
  11908. },
  11909. YYYYYY : function () {
  11910. var y = this.year(), sign = y >= 0 ? '+' : '-';
  11911. return sign + leftZeroFill(Math.abs(y), 6);
  11912. },
  11913. gg : function () {
  11914. return leftZeroFill(this.weekYear() % 100, 2);
  11915. },
  11916. gggg : function () {
  11917. return this.weekYear();
  11918. },
  11919. ggggg : function () {
  11920. return leftZeroFill(this.weekYear(), 5);
  11921. },
  11922. GG : function () {
  11923. return leftZeroFill(this.isoWeekYear() % 100, 2);
  11924. },
  11925. GGGG : function () {
  11926. return this.isoWeekYear();
  11927. },
  11928. GGGGG : function () {
  11929. return leftZeroFill(this.isoWeekYear(), 5);
  11930. },
  11931. e : function () {
  11932. return this.weekday();
  11933. },
  11934. E : function () {
  11935. return this.isoWeekday();
  11936. },
  11937. a : function () {
  11938. return this.lang().meridiem(this.hours(), this.minutes(), true);
  11939. },
  11940. A : function () {
  11941. return this.lang().meridiem(this.hours(), this.minutes(), false);
  11942. },
  11943. H : function () {
  11944. return this.hours();
  11945. },
  11946. h : function () {
  11947. return this.hours() % 12 || 12;
  11948. },
  11949. m : function () {
  11950. return this.minutes();
  11951. },
  11952. s : function () {
  11953. return this.seconds();
  11954. },
  11955. S : function () {
  11956. return toInt(this.milliseconds() / 100);
  11957. },
  11958. SS : function () {
  11959. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  11960. },
  11961. SSS : function () {
  11962. return leftZeroFill(this.milliseconds(), 3);
  11963. },
  11964. SSSS : function () {
  11965. return leftZeroFill(this.milliseconds(), 3);
  11966. },
  11967. Z : function () {
  11968. var a = -this.zone(),
  11969. b = "+";
  11970. if (a < 0) {
  11971. a = -a;
  11972. b = "-";
  11973. }
  11974. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  11975. },
  11976. ZZ : function () {
  11977. var a = -this.zone(),
  11978. b = "+";
  11979. if (a < 0) {
  11980. a = -a;
  11981. b = "-";
  11982. }
  11983. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  11984. },
  11985. z : function () {
  11986. return this.zoneAbbr();
  11987. },
  11988. zz : function () {
  11989. return this.zoneName();
  11990. },
  11991. X : function () {
  11992. return this.unix();
  11993. },
  11994. Q : function () {
  11995. return this.quarter();
  11996. }
  11997. },
  11998. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  11999. function padToken(func, count) {
  12000. return function (a) {
  12001. return leftZeroFill(func.call(this, a), count);
  12002. };
  12003. }
  12004. function ordinalizeToken(func, period) {
  12005. return function (a) {
  12006. return this.lang().ordinal(func.call(this, a), period);
  12007. };
  12008. }
  12009. while (ordinalizeTokens.length) {
  12010. i = ordinalizeTokens.pop();
  12011. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  12012. }
  12013. while (paddedTokens.length) {
  12014. i = paddedTokens.pop();
  12015. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  12016. }
  12017. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  12018. /************************************
  12019. Constructors
  12020. ************************************/
  12021. function Language() {
  12022. }
  12023. // Moment prototype object
  12024. function Moment(config) {
  12025. checkOverflow(config);
  12026. extend(this, config);
  12027. }
  12028. // Duration Constructor
  12029. function Duration(duration) {
  12030. var normalizedInput = normalizeObjectUnits(duration),
  12031. years = normalizedInput.year || 0,
  12032. months = normalizedInput.month || 0,
  12033. weeks = normalizedInput.week || 0,
  12034. days = normalizedInput.day || 0,
  12035. hours = normalizedInput.hour || 0,
  12036. minutes = normalizedInput.minute || 0,
  12037. seconds = normalizedInput.second || 0,
  12038. milliseconds = normalizedInput.millisecond || 0;
  12039. // representation for dateAddRemove
  12040. this._milliseconds = +milliseconds +
  12041. seconds * 1e3 + // 1000
  12042. minutes * 6e4 + // 1000 * 60
  12043. hours * 36e5; // 1000 * 60 * 60
  12044. // Because of dateAddRemove treats 24 hours as different from a
  12045. // day when working around DST, we need to store them separately
  12046. this._days = +days +
  12047. weeks * 7;
  12048. // It is impossible translate months into days without knowing
  12049. // which months you are are talking about, so we have to store
  12050. // it separately.
  12051. this._months = +months +
  12052. years * 12;
  12053. this._data = {};
  12054. this._bubble();
  12055. }
  12056. /************************************
  12057. Helpers
  12058. ************************************/
  12059. function extend(a, b) {
  12060. for (var i in b) {
  12061. if (b.hasOwnProperty(i)) {
  12062. a[i] = b[i];
  12063. }
  12064. }
  12065. if (b.hasOwnProperty("toString")) {
  12066. a.toString = b.toString;
  12067. }
  12068. if (b.hasOwnProperty("valueOf")) {
  12069. a.valueOf = b.valueOf;
  12070. }
  12071. return a;
  12072. }
  12073. function absRound(number) {
  12074. if (number < 0) {
  12075. return Math.ceil(number);
  12076. } else {
  12077. return Math.floor(number);
  12078. }
  12079. }
  12080. // left zero fill a number
  12081. // see http://jsperf.com/left-zero-filling for performance comparison
  12082. function leftZeroFill(number, targetLength, forceSign) {
  12083. var output = Math.abs(number) + '',
  12084. sign = number >= 0;
  12085. while (output.length < targetLength) {
  12086. output = '0' + output;
  12087. }
  12088. return (sign ? (forceSign ? '+' : '') : '-') + output;
  12089. }
  12090. // helper function for _.addTime and _.subtractTime
  12091. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  12092. var milliseconds = duration._milliseconds,
  12093. days = duration._days,
  12094. months = duration._months,
  12095. minutes,
  12096. hours;
  12097. if (milliseconds) {
  12098. mom._d.setTime(+mom._d + milliseconds * isAdding);
  12099. }
  12100. // store the minutes and hours so we can restore them
  12101. if (days || months) {
  12102. minutes = mom.minute();
  12103. hours = mom.hour();
  12104. }
  12105. if (days) {
  12106. mom.date(mom.date() + days * isAdding);
  12107. }
  12108. if (months) {
  12109. mom.month(mom.month() + months * isAdding);
  12110. }
  12111. if (milliseconds && !ignoreUpdateOffset) {
  12112. moment.updateOffset(mom);
  12113. }
  12114. // restore the minutes and hours after possibly changing dst
  12115. if (days || months) {
  12116. mom.minute(minutes);
  12117. mom.hour(hours);
  12118. }
  12119. }
  12120. // check if is an array
  12121. function isArray(input) {
  12122. return Object.prototype.toString.call(input) === '[object Array]';
  12123. }
  12124. function isDate(input) {
  12125. return Object.prototype.toString.call(input) === '[object Date]' ||
  12126. input instanceof Date;
  12127. }
  12128. // compare two arrays, return the number of differences
  12129. function compareArrays(array1, array2, dontConvert) {
  12130. var len = Math.min(array1.length, array2.length),
  12131. lengthDiff = Math.abs(array1.length - array2.length),
  12132. diffs = 0,
  12133. i;
  12134. for (i = 0; i < len; i++) {
  12135. if ((dontConvert && array1[i] !== array2[i]) ||
  12136. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  12137. diffs++;
  12138. }
  12139. }
  12140. return diffs + lengthDiff;
  12141. }
  12142. function normalizeUnits(units) {
  12143. if (units) {
  12144. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  12145. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  12146. }
  12147. return units;
  12148. }
  12149. function normalizeObjectUnits(inputObject) {
  12150. var normalizedInput = {},
  12151. normalizedProp,
  12152. prop;
  12153. for (prop in inputObject) {
  12154. if (inputObject.hasOwnProperty(prop)) {
  12155. normalizedProp = normalizeUnits(prop);
  12156. if (normalizedProp) {
  12157. normalizedInput[normalizedProp] = inputObject[prop];
  12158. }
  12159. }
  12160. }
  12161. return normalizedInput;
  12162. }
  12163. function makeList(field) {
  12164. var count, setter;
  12165. if (field.indexOf('week') === 0) {
  12166. count = 7;
  12167. setter = 'day';
  12168. }
  12169. else if (field.indexOf('month') === 0) {
  12170. count = 12;
  12171. setter = 'month';
  12172. }
  12173. else {
  12174. return;
  12175. }
  12176. moment[field] = function (format, index) {
  12177. var i, getter,
  12178. method = moment.fn._lang[field],
  12179. results = [];
  12180. if (typeof format === 'number') {
  12181. index = format;
  12182. format = undefined;
  12183. }
  12184. getter = function (i) {
  12185. var m = moment().utc().set(setter, i);
  12186. return method.call(moment.fn._lang, m, format || '');
  12187. };
  12188. if (index != null) {
  12189. return getter(index);
  12190. }
  12191. else {
  12192. for (i = 0; i < count; i++) {
  12193. results.push(getter(i));
  12194. }
  12195. return results;
  12196. }
  12197. };
  12198. }
  12199. function toInt(argumentForCoercion) {
  12200. var coercedNumber = +argumentForCoercion,
  12201. value = 0;
  12202. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  12203. if (coercedNumber >= 0) {
  12204. value = Math.floor(coercedNumber);
  12205. } else {
  12206. value = Math.ceil(coercedNumber);
  12207. }
  12208. }
  12209. return value;
  12210. }
  12211. function daysInMonth(year, month) {
  12212. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  12213. }
  12214. function daysInYear(year) {
  12215. return isLeapYear(year) ? 366 : 365;
  12216. }
  12217. function isLeapYear(year) {
  12218. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  12219. }
  12220. function checkOverflow(m) {
  12221. var overflow;
  12222. if (m._a && m._pf.overflow === -2) {
  12223. overflow =
  12224. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  12225. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  12226. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  12227. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  12228. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  12229. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  12230. -1;
  12231. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  12232. overflow = DATE;
  12233. }
  12234. m._pf.overflow = overflow;
  12235. }
  12236. }
  12237. function initializeParsingFlags(config) {
  12238. config._pf = {
  12239. empty : false,
  12240. unusedTokens : [],
  12241. unusedInput : [],
  12242. overflow : -2,
  12243. charsLeftOver : 0,
  12244. nullInput : false,
  12245. invalidMonth : null,
  12246. invalidFormat : false,
  12247. userInvalidated : false,
  12248. iso: false
  12249. };
  12250. }
  12251. function isValid(m) {
  12252. if (m._isValid == null) {
  12253. m._isValid = !isNaN(m._d.getTime()) &&
  12254. m._pf.overflow < 0 &&
  12255. !m._pf.empty &&
  12256. !m._pf.invalidMonth &&
  12257. !m._pf.nullInput &&
  12258. !m._pf.invalidFormat &&
  12259. !m._pf.userInvalidated;
  12260. if (m._strict) {
  12261. m._isValid = m._isValid &&
  12262. m._pf.charsLeftOver === 0 &&
  12263. m._pf.unusedTokens.length === 0;
  12264. }
  12265. }
  12266. return m._isValid;
  12267. }
  12268. function normalizeLanguage(key) {
  12269. return key ? key.toLowerCase().replace('_', '-') : key;
  12270. }
  12271. // Return a moment from input, that is local/utc/zone equivalent to model.
  12272. function makeAs(input, model) {
  12273. return model._isUTC ? moment(input).zone(model._offset || 0) :
  12274. moment(input).local();
  12275. }
  12276. /************************************
  12277. Languages
  12278. ************************************/
  12279. extend(Language.prototype, {
  12280. set : function (config) {
  12281. var prop, i;
  12282. for (i in config) {
  12283. prop = config[i];
  12284. if (typeof prop === 'function') {
  12285. this[i] = prop;
  12286. } else {
  12287. this['_' + i] = prop;
  12288. }
  12289. }
  12290. },
  12291. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  12292. months : function (m) {
  12293. return this._months[m.month()];
  12294. },
  12295. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  12296. monthsShort : function (m) {
  12297. return this._monthsShort[m.month()];
  12298. },
  12299. monthsParse : function (monthName) {
  12300. var i, mom, regex;
  12301. if (!this._monthsParse) {
  12302. this._monthsParse = [];
  12303. }
  12304. for (i = 0; i < 12; i++) {
  12305. // make the regex if we don't have it already
  12306. if (!this._monthsParse[i]) {
  12307. mom = moment.utc([2000, i]);
  12308. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  12309. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  12310. }
  12311. // test the regex
  12312. if (this._monthsParse[i].test(monthName)) {
  12313. return i;
  12314. }
  12315. }
  12316. },
  12317. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  12318. weekdays : function (m) {
  12319. return this._weekdays[m.day()];
  12320. },
  12321. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  12322. weekdaysShort : function (m) {
  12323. return this._weekdaysShort[m.day()];
  12324. },
  12325. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  12326. weekdaysMin : function (m) {
  12327. return this._weekdaysMin[m.day()];
  12328. },
  12329. weekdaysParse : function (weekdayName) {
  12330. var i, mom, regex;
  12331. if (!this._weekdaysParse) {
  12332. this._weekdaysParse = [];
  12333. }
  12334. for (i = 0; i < 7; i++) {
  12335. // make the regex if we don't have it already
  12336. if (!this._weekdaysParse[i]) {
  12337. mom = moment([2000, 1]).day(i);
  12338. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  12339. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  12340. }
  12341. // test the regex
  12342. if (this._weekdaysParse[i].test(weekdayName)) {
  12343. return i;
  12344. }
  12345. }
  12346. },
  12347. _longDateFormat : {
  12348. LT : "h:mm A",
  12349. L : "MM/DD/YYYY",
  12350. LL : "MMMM D YYYY",
  12351. LLL : "MMMM D YYYY LT",
  12352. LLLL : "dddd, MMMM D YYYY LT"
  12353. },
  12354. longDateFormat : function (key) {
  12355. var output = this._longDateFormat[key];
  12356. if (!output && this._longDateFormat[key.toUpperCase()]) {
  12357. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  12358. return val.slice(1);
  12359. });
  12360. this._longDateFormat[key] = output;
  12361. }
  12362. return output;
  12363. },
  12364. isPM : function (input) {
  12365. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  12366. // Using charAt should be more compatible.
  12367. return ((input + '').toLowerCase().charAt(0) === 'p');
  12368. },
  12369. _meridiemParse : /[ap]\.?m?\.?/i,
  12370. meridiem : function (hours, minutes, isLower) {
  12371. if (hours > 11) {
  12372. return isLower ? 'pm' : 'PM';
  12373. } else {
  12374. return isLower ? 'am' : 'AM';
  12375. }
  12376. },
  12377. _calendar : {
  12378. sameDay : '[Today at] LT',
  12379. nextDay : '[Tomorrow at] LT',
  12380. nextWeek : 'dddd [at] LT',
  12381. lastDay : '[Yesterday at] LT',
  12382. lastWeek : '[Last] dddd [at] LT',
  12383. sameElse : 'L'
  12384. },
  12385. calendar : function (key, mom) {
  12386. var output = this._calendar[key];
  12387. return typeof output === 'function' ? output.apply(mom) : output;
  12388. },
  12389. _relativeTime : {
  12390. future : "in %s",
  12391. past : "%s ago",
  12392. s : "a few seconds",
  12393. m : "a minute",
  12394. mm : "%d minutes",
  12395. h : "an hour",
  12396. hh : "%d hours",
  12397. d : "a day",
  12398. dd : "%d days",
  12399. M : "a month",
  12400. MM : "%d months",
  12401. y : "a year",
  12402. yy : "%d years"
  12403. },
  12404. relativeTime : function (number, withoutSuffix, string, isFuture) {
  12405. var output = this._relativeTime[string];
  12406. return (typeof output === 'function') ?
  12407. output(number, withoutSuffix, string, isFuture) :
  12408. output.replace(/%d/i, number);
  12409. },
  12410. pastFuture : function (diff, output) {
  12411. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  12412. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  12413. },
  12414. ordinal : function (number) {
  12415. return this._ordinal.replace("%d", number);
  12416. },
  12417. _ordinal : "%d",
  12418. preparse : function (string) {
  12419. return string;
  12420. },
  12421. postformat : function (string) {
  12422. return string;
  12423. },
  12424. week : function (mom) {
  12425. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  12426. },
  12427. _week : {
  12428. dow : 0, // Sunday is the first day of the week.
  12429. doy : 6 // The week that contains Jan 1st is the first week of the year.
  12430. },
  12431. _invalidDate: 'Invalid date',
  12432. invalidDate: function () {
  12433. return this._invalidDate;
  12434. }
  12435. });
  12436. // Loads a language definition into the `languages` cache. The function
  12437. // takes a key and optionally values. If not in the browser and no values
  12438. // are provided, it will load the language file module. As a convenience,
  12439. // this function also returns the language values.
  12440. function loadLang(key, values) {
  12441. values.abbr = key;
  12442. if (!languages[key]) {
  12443. languages[key] = new Language();
  12444. }
  12445. languages[key].set(values);
  12446. return languages[key];
  12447. }
  12448. // Remove a language from the `languages` cache. Mostly useful in tests.
  12449. function unloadLang(key) {
  12450. delete languages[key];
  12451. }
  12452. // Determines which language definition to use and returns it.
  12453. //
  12454. // With no parameters, it will return the global language. If you
  12455. // pass in a language key, such as 'en', it will return the
  12456. // definition for 'en', so long as 'en' has already been loaded using
  12457. // moment.lang.
  12458. function getLangDefinition(key) {
  12459. var i = 0, j, lang, next, split,
  12460. get = function (k) {
  12461. if (!languages[k] && hasModule) {
  12462. try {
  12463. require('./lang/' + k);
  12464. } catch (e) { }
  12465. }
  12466. return languages[k];
  12467. };
  12468. if (!key) {
  12469. return moment.fn._lang;
  12470. }
  12471. if (!isArray(key)) {
  12472. //short-circuit everything else
  12473. lang = get(key);
  12474. if (lang) {
  12475. return lang;
  12476. }
  12477. key = [key];
  12478. }
  12479. //pick the language from the array
  12480. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  12481. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  12482. while (i < key.length) {
  12483. split = normalizeLanguage(key[i]).split('-');
  12484. j = split.length;
  12485. next = normalizeLanguage(key[i + 1]);
  12486. next = next ? next.split('-') : null;
  12487. while (j > 0) {
  12488. lang = get(split.slice(0, j).join('-'));
  12489. if (lang) {
  12490. return lang;
  12491. }
  12492. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  12493. //the next array item is better than a shallower substring of this one
  12494. break;
  12495. }
  12496. j--;
  12497. }
  12498. i++;
  12499. }
  12500. return moment.fn._lang;
  12501. }
  12502. /************************************
  12503. Formatting
  12504. ************************************/
  12505. function removeFormattingTokens(input) {
  12506. if (input.match(/\[[\s\S]/)) {
  12507. return input.replace(/^\[|\]$/g, "");
  12508. }
  12509. return input.replace(/\\/g, "");
  12510. }
  12511. function makeFormatFunction(format) {
  12512. var array = format.match(formattingTokens), i, length;
  12513. for (i = 0, length = array.length; i < length; i++) {
  12514. if (formatTokenFunctions[array[i]]) {
  12515. array[i] = formatTokenFunctions[array[i]];
  12516. } else {
  12517. array[i] = removeFormattingTokens(array[i]);
  12518. }
  12519. }
  12520. return function (mom) {
  12521. var output = "";
  12522. for (i = 0; i < length; i++) {
  12523. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  12524. }
  12525. return output;
  12526. };
  12527. }
  12528. // format date using native date object
  12529. function formatMoment(m, format) {
  12530. if (!m.isValid()) {
  12531. return m.lang().invalidDate();
  12532. }
  12533. format = expandFormat(format, m.lang());
  12534. if (!formatFunctions[format]) {
  12535. formatFunctions[format] = makeFormatFunction(format);
  12536. }
  12537. return formatFunctions[format](m);
  12538. }
  12539. function expandFormat(format, lang) {
  12540. var i = 5;
  12541. function replaceLongDateFormatTokens(input) {
  12542. return lang.longDateFormat(input) || input;
  12543. }
  12544. localFormattingTokens.lastIndex = 0;
  12545. while (i >= 0 && localFormattingTokens.test(format)) {
  12546. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  12547. localFormattingTokens.lastIndex = 0;
  12548. i -= 1;
  12549. }
  12550. return format;
  12551. }
  12552. /************************************
  12553. Parsing
  12554. ************************************/
  12555. // get the regex to find the next token
  12556. function getParseRegexForToken(token, config) {
  12557. var a, strict = config._strict;
  12558. switch (token) {
  12559. case 'DDDD':
  12560. return parseTokenThreeDigits;
  12561. case 'YYYY':
  12562. case 'GGGG':
  12563. case 'gggg':
  12564. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  12565. case 'YYYYYY':
  12566. case 'YYYYY':
  12567. case 'GGGGG':
  12568. case 'ggggg':
  12569. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  12570. case 'S':
  12571. if (strict) { return parseTokenOneDigit; }
  12572. /* falls through */
  12573. case 'SS':
  12574. if (strict) { return parseTokenTwoDigits; }
  12575. /* falls through */
  12576. case 'SSS':
  12577. case 'DDD':
  12578. return strict ? parseTokenThreeDigits : parseTokenOneToThreeDigits;
  12579. case 'MMM':
  12580. case 'MMMM':
  12581. case 'dd':
  12582. case 'ddd':
  12583. case 'dddd':
  12584. return parseTokenWord;
  12585. case 'a':
  12586. case 'A':
  12587. return getLangDefinition(config._l)._meridiemParse;
  12588. case 'X':
  12589. return parseTokenTimestampMs;
  12590. case 'Z':
  12591. case 'ZZ':
  12592. return parseTokenTimezone;
  12593. case 'T':
  12594. return parseTokenT;
  12595. case 'SSSS':
  12596. return parseTokenDigits;
  12597. case 'MM':
  12598. case 'DD':
  12599. case 'YY':
  12600. case 'GG':
  12601. case 'gg':
  12602. case 'HH':
  12603. case 'hh':
  12604. case 'mm':
  12605. case 'ss':
  12606. case 'ww':
  12607. case 'WW':
  12608. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  12609. case 'M':
  12610. case 'D':
  12611. case 'd':
  12612. case 'H':
  12613. case 'h':
  12614. case 'm':
  12615. case 's':
  12616. case 'w':
  12617. case 'W':
  12618. case 'e':
  12619. case 'E':
  12620. return strict ? parseTokenOneDigit : parseTokenOneOrTwoDigits;
  12621. default :
  12622. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  12623. return a;
  12624. }
  12625. }
  12626. function timezoneMinutesFromString(string) {
  12627. string = string || "";
  12628. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  12629. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  12630. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  12631. minutes = +(parts[1] * 60) + toInt(parts[2]);
  12632. return parts[0] === '+' ? -minutes : minutes;
  12633. }
  12634. // function to convert string input to date
  12635. function addTimeToArrayFromToken(token, input, config) {
  12636. var a, datePartArray = config._a;
  12637. switch (token) {
  12638. // MONTH
  12639. case 'M' : // fall through to MM
  12640. case 'MM' :
  12641. if (input != null) {
  12642. datePartArray[MONTH] = toInt(input) - 1;
  12643. }
  12644. break;
  12645. case 'MMM' : // fall through to MMMM
  12646. case 'MMMM' :
  12647. a = getLangDefinition(config._l).monthsParse(input);
  12648. // if we didn't find a month name, mark the date as invalid.
  12649. if (a != null) {
  12650. datePartArray[MONTH] = a;
  12651. } else {
  12652. config._pf.invalidMonth = input;
  12653. }
  12654. break;
  12655. // DAY OF MONTH
  12656. case 'D' : // fall through to DD
  12657. case 'DD' :
  12658. if (input != null) {
  12659. datePartArray[DATE] = toInt(input);
  12660. }
  12661. break;
  12662. // DAY OF YEAR
  12663. case 'DDD' : // fall through to DDDD
  12664. case 'DDDD' :
  12665. if (input != null) {
  12666. config._dayOfYear = toInt(input);
  12667. }
  12668. break;
  12669. // YEAR
  12670. case 'YY' :
  12671. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  12672. break;
  12673. case 'YYYY' :
  12674. case 'YYYYY' :
  12675. case 'YYYYYY' :
  12676. datePartArray[YEAR] = toInt(input);
  12677. break;
  12678. // AM / PM
  12679. case 'a' : // fall through to A
  12680. case 'A' :
  12681. config._isPm = getLangDefinition(config._l).isPM(input);
  12682. break;
  12683. // 24 HOUR
  12684. case 'H' : // fall through to hh
  12685. case 'HH' : // fall through to hh
  12686. case 'h' : // fall through to hh
  12687. case 'hh' :
  12688. datePartArray[HOUR] = toInt(input);
  12689. break;
  12690. // MINUTE
  12691. case 'm' : // fall through to mm
  12692. case 'mm' :
  12693. datePartArray[MINUTE] = toInt(input);
  12694. break;
  12695. // SECOND
  12696. case 's' : // fall through to ss
  12697. case 'ss' :
  12698. datePartArray[SECOND] = toInt(input);
  12699. break;
  12700. // MILLISECOND
  12701. case 'S' :
  12702. case 'SS' :
  12703. case 'SSS' :
  12704. case 'SSSS' :
  12705. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  12706. break;
  12707. // UNIX TIMESTAMP WITH MS
  12708. case 'X':
  12709. config._d = new Date(parseFloat(input) * 1000);
  12710. break;
  12711. // TIMEZONE
  12712. case 'Z' : // fall through to ZZ
  12713. case 'ZZ' :
  12714. config._useUTC = true;
  12715. config._tzm = timezoneMinutesFromString(input);
  12716. break;
  12717. case 'w':
  12718. case 'ww':
  12719. case 'W':
  12720. case 'WW':
  12721. case 'd':
  12722. case 'dd':
  12723. case 'ddd':
  12724. case 'dddd':
  12725. case 'e':
  12726. case 'E':
  12727. token = token.substr(0, 1);
  12728. /* falls through */
  12729. case 'gg':
  12730. case 'gggg':
  12731. case 'GG':
  12732. case 'GGGG':
  12733. case 'GGGGG':
  12734. token = token.substr(0, 2);
  12735. if (input) {
  12736. config._w = config._w || {};
  12737. config._w[token] = input;
  12738. }
  12739. break;
  12740. }
  12741. }
  12742. // convert an array to a date.
  12743. // the array should mirror the parameters below
  12744. // note: all values past the year are optional and will default to the lowest possible value.
  12745. // [year, month, day , hour, minute, second, millisecond]
  12746. function dateFromConfig(config) {
  12747. var i, date, input = [], currentDate,
  12748. yearToUse, fixYear, w, temp, lang, weekday, week;
  12749. if (config._d) {
  12750. return;
  12751. }
  12752. currentDate = currentDateArray(config);
  12753. //compute day of the year from weeks and weekdays
  12754. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  12755. fixYear = function (val) {
  12756. var int_val = parseInt(val, 10);
  12757. return val ?
  12758. (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
  12759. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  12760. };
  12761. w = config._w;
  12762. if (w.GG != null || w.W != null || w.E != null) {
  12763. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  12764. }
  12765. else {
  12766. lang = getLangDefinition(config._l);
  12767. weekday = w.d != null ? parseWeekday(w.d, lang) :
  12768. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  12769. week = parseInt(w.w, 10) || 1;
  12770. //if we're parsing 'd', then the low day numbers may be next week
  12771. if (w.d != null && weekday < lang._week.dow) {
  12772. week++;
  12773. }
  12774. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  12775. }
  12776. config._a[YEAR] = temp.year;
  12777. config._dayOfYear = temp.dayOfYear;
  12778. }
  12779. //if the day of the year is set, figure out what it is
  12780. if (config._dayOfYear) {
  12781. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  12782. if (config._dayOfYear > daysInYear(yearToUse)) {
  12783. config._pf._overflowDayOfYear = true;
  12784. }
  12785. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  12786. config._a[MONTH] = date.getUTCMonth();
  12787. config._a[DATE] = date.getUTCDate();
  12788. }
  12789. // Default to current date.
  12790. // * if no year, month, day of month are given, default to today
  12791. // * if day of month is given, default month and year
  12792. // * if month is given, default only year
  12793. // * if year is given, don't default anything
  12794. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  12795. config._a[i] = input[i] = currentDate[i];
  12796. }
  12797. // Zero out whatever was not defaulted, including time
  12798. for (; i < 7; i++) {
  12799. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  12800. }
  12801. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  12802. input[HOUR] += toInt((config._tzm || 0) / 60);
  12803. input[MINUTE] += toInt((config._tzm || 0) % 60);
  12804. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  12805. }
  12806. function dateFromObject(config) {
  12807. var normalizedInput;
  12808. if (config._d) {
  12809. return;
  12810. }
  12811. normalizedInput = normalizeObjectUnits(config._i);
  12812. config._a = [
  12813. normalizedInput.year,
  12814. normalizedInput.month,
  12815. normalizedInput.day,
  12816. normalizedInput.hour,
  12817. normalizedInput.minute,
  12818. normalizedInput.second,
  12819. normalizedInput.millisecond
  12820. ];
  12821. dateFromConfig(config);
  12822. }
  12823. function currentDateArray(config) {
  12824. var now = new Date();
  12825. if (config._useUTC) {
  12826. return [
  12827. now.getUTCFullYear(),
  12828. now.getUTCMonth(),
  12829. now.getUTCDate()
  12830. ];
  12831. } else {
  12832. return [now.getFullYear(), now.getMonth(), now.getDate()];
  12833. }
  12834. }
  12835. // date from string and format string
  12836. function makeDateFromStringAndFormat(config) {
  12837. config._a = [];
  12838. config._pf.empty = true;
  12839. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  12840. var lang = getLangDefinition(config._l),
  12841. string = '' + config._i,
  12842. i, parsedInput, tokens, token, skipped,
  12843. stringLength = string.length,
  12844. totalParsedInputLength = 0;
  12845. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  12846. for (i = 0; i < tokens.length; i++) {
  12847. token = tokens[i];
  12848. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  12849. if (parsedInput) {
  12850. skipped = string.substr(0, string.indexOf(parsedInput));
  12851. if (skipped.length > 0) {
  12852. config._pf.unusedInput.push(skipped);
  12853. }
  12854. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  12855. totalParsedInputLength += parsedInput.length;
  12856. }
  12857. // don't parse if it's not a known token
  12858. if (formatTokenFunctions[token]) {
  12859. if (parsedInput) {
  12860. config._pf.empty = false;
  12861. }
  12862. else {
  12863. config._pf.unusedTokens.push(token);
  12864. }
  12865. addTimeToArrayFromToken(token, parsedInput, config);
  12866. }
  12867. else if (config._strict && !parsedInput) {
  12868. config._pf.unusedTokens.push(token);
  12869. }
  12870. }
  12871. // add remaining unparsed input length to the string
  12872. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  12873. if (string.length > 0) {
  12874. config._pf.unusedInput.push(string);
  12875. }
  12876. // handle am pm
  12877. if (config._isPm && config._a[HOUR] < 12) {
  12878. config._a[HOUR] += 12;
  12879. }
  12880. // if is 12 am, change hours to 0
  12881. if (config._isPm === false && config._a[HOUR] === 12) {
  12882. config._a[HOUR] = 0;
  12883. }
  12884. dateFromConfig(config);
  12885. checkOverflow(config);
  12886. }
  12887. function unescapeFormat(s) {
  12888. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  12889. return p1 || p2 || p3 || p4;
  12890. });
  12891. }
  12892. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  12893. function regexpEscape(s) {
  12894. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  12895. }
  12896. // date from string and array of format strings
  12897. function makeDateFromStringAndArray(config) {
  12898. var tempConfig,
  12899. bestMoment,
  12900. scoreToBeat,
  12901. i,
  12902. currentScore;
  12903. if (config._f.length === 0) {
  12904. config._pf.invalidFormat = true;
  12905. config._d = new Date(NaN);
  12906. return;
  12907. }
  12908. for (i = 0; i < config._f.length; i++) {
  12909. currentScore = 0;
  12910. tempConfig = extend({}, config);
  12911. initializeParsingFlags(tempConfig);
  12912. tempConfig._f = config._f[i];
  12913. makeDateFromStringAndFormat(tempConfig);
  12914. if (!isValid(tempConfig)) {
  12915. continue;
  12916. }
  12917. // if there is any input that was not parsed add a penalty for that format
  12918. currentScore += tempConfig._pf.charsLeftOver;
  12919. //or tokens
  12920. currentScore += tempConfig._pf.unusedTokens.length * 10;
  12921. tempConfig._pf.score = currentScore;
  12922. if (scoreToBeat == null || currentScore < scoreToBeat) {
  12923. scoreToBeat = currentScore;
  12924. bestMoment = tempConfig;
  12925. }
  12926. }
  12927. extend(config, bestMoment || tempConfig);
  12928. }
  12929. // date from iso format
  12930. function makeDateFromString(config) {
  12931. var i,
  12932. string = config._i,
  12933. match = isoRegex.exec(string);
  12934. if (match) {
  12935. config._pf.iso = true;
  12936. for (i = 4; i > 0; i--) {
  12937. if (match[i]) {
  12938. // match[5] should be "T" or undefined
  12939. config._f = isoDates[i - 1] + (match[6] || " ");
  12940. break;
  12941. }
  12942. }
  12943. for (i = 0; i < 4; i++) {
  12944. if (isoTimes[i][1].exec(string)) {
  12945. config._f += isoTimes[i][0];
  12946. break;
  12947. }
  12948. }
  12949. if (string.match(parseTokenTimezone)) {
  12950. config._f += "Z";
  12951. }
  12952. makeDateFromStringAndFormat(config);
  12953. }
  12954. else {
  12955. config._d = new Date(string);
  12956. }
  12957. }
  12958. function makeDateFromInput(config) {
  12959. var input = config._i,
  12960. matched = aspNetJsonRegex.exec(input);
  12961. if (input === undefined) {
  12962. config._d = new Date();
  12963. } else if (matched) {
  12964. config._d = new Date(+matched[1]);
  12965. } else if (typeof input === 'string') {
  12966. makeDateFromString(config);
  12967. } else if (isArray(input)) {
  12968. config._a = input.slice(0);
  12969. dateFromConfig(config);
  12970. } else if (isDate(input)) {
  12971. config._d = new Date(+input);
  12972. } else if (typeof(input) === 'object') {
  12973. dateFromObject(config);
  12974. } else {
  12975. config._d = new Date(input);
  12976. }
  12977. }
  12978. function makeDate(y, m, d, h, M, s, ms) {
  12979. //can't just apply() to create a date:
  12980. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  12981. var date = new Date(y, m, d, h, M, s, ms);
  12982. //the date constructor doesn't accept years < 1970
  12983. if (y < 1970) {
  12984. date.setFullYear(y);
  12985. }
  12986. return date;
  12987. }
  12988. function makeUTCDate(y) {
  12989. var date = new Date(Date.UTC.apply(null, arguments));
  12990. if (y < 1970) {
  12991. date.setUTCFullYear(y);
  12992. }
  12993. return date;
  12994. }
  12995. function parseWeekday(input, language) {
  12996. if (typeof input === 'string') {
  12997. if (!isNaN(input)) {
  12998. input = parseInt(input, 10);
  12999. }
  13000. else {
  13001. input = language.weekdaysParse(input);
  13002. if (typeof input !== 'number') {
  13003. return null;
  13004. }
  13005. }
  13006. }
  13007. return input;
  13008. }
  13009. /************************************
  13010. Relative Time
  13011. ************************************/
  13012. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  13013. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  13014. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  13015. }
  13016. function relativeTime(milliseconds, withoutSuffix, lang) {
  13017. var seconds = round(Math.abs(milliseconds) / 1000),
  13018. minutes = round(seconds / 60),
  13019. hours = round(minutes / 60),
  13020. days = round(hours / 24),
  13021. years = round(days / 365),
  13022. args = seconds < 45 && ['s', seconds] ||
  13023. minutes === 1 && ['m'] ||
  13024. minutes < 45 && ['mm', minutes] ||
  13025. hours === 1 && ['h'] ||
  13026. hours < 22 && ['hh', hours] ||
  13027. days === 1 && ['d'] ||
  13028. days <= 25 && ['dd', days] ||
  13029. days <= 45 && ['M'] ||
  13030. days < 345 && ['MM', round(days / 30)] ||
  13031. years === 1 && ['y'] || ['yy', years];
  13032. args[2] = withoutSuffix;
  13033. args[3] = milliseconds > 0;
  13034. args[4] = lang;
  13035. return substituteTimeAgo.apply({}, args);
  13036. }
  13037. /************************************
  13038. Week of Year
  13039. ************************************/
  13040. // firstDayOfWeek 0 = sun, 6 = sat
  13041. // the day of the week that starts the week
  13042. // (usually sunday or monday)
  13043. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  13044. // the first week is the week that contains the first
  13045. // of this day of the week
  13046. // (eg. ISO weeks use thursday (4))
  13047. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  13048. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  13049. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  13050. adjustedMoment;
  13051. if (daysToDayOfWeek > end) {
  13052. daysToDayOfWeek -= 7;
  13053. }
  13054. if (daysToDayOfWeek < end - 7) {
  13055. daysToDayOfWeek += 7;
  13056. }
  13057. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  13058. return {
  13059. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  13060. year: adjustedMoment.year()
  13061. };
  13062. }
  13063. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  13064. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  13065. // The only solid way to create an iso date from year is to use
  13066. // a string format (Date.UTC handles only years > 1900). Don't ask why
  13067. // it doesn't need Z at the end.
  13068. var d = new Date(leftZeroFill(year, 6, true) + '-01-01').getUTCDay(),
  13069. daysToAdd, dayOfYear;
  13070. weekday = weekday != null ? weekday : firstDayOfWeek;
  13071. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0);
  13072. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  13073. return {
  13074. year: dayOfYear > 0 ? year : year - 1,
  13075. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  13076. };
  13077. }
  13078. /************************************
  13079. Top Level Functions
  13080. ************************************/
  13081. function makeMoment(config) {
  13082. var input = config._i,
  13083. format = config._f;
  13084. if (typeof config._pf === 'undefined') {
  13085. initializeParsingFlags(config);
  13086. }
  13087. if (input === null) {
  13088. return moment.invalid({nullInput: true});
  13089. }
  13090. if (typeof input === 'string') {
  13091. config._i = input = getLangDefinition().preparse(input);
  13092. }
  13093. if (moment.isMoment(input)) {
  13094. config = extend({}, input);
  13095. config._d = new Date(+input._d);
  13096. } else if (format) {
  13097. if (isArray(format)) {
  13098. makeDateFromStringAndArray(config);
  13099. } else {
  13100. makeDateFromStringAndFormat(config);
  13101. }
  13102. } else {
  13103. makeDateFromInput(config);
  13104. }
  13105. return new Moment(config);
  13106. }
  13107. moment = function (input, format, lang, strict) {
  13108. if (typeof(lang) === "boolean") {
  13109. strict = lang;
  13110. lang = undefined;
  13111. }
  13112. return makeMoment({
  13113. _i : input,
  13114. _f : format,
  13115. _l : lang,
  13116. _strict : strict,
  13117. _isUTC : false
  13118. });
  13119. };
  13120. // creating with utc
  13121. moment.utc = function (input, format, lang, strict) {
  13122. var m;
  13123. if (typeof(lang) === "boolean") {
  13124. strict = lang;
  13125. lang = undefined;
  13126. }
  13127. m = makeMoment({
  13128. _useUTC : true,
  13129. _isUTC : true,
  13130. _l : lang,
  13131. _i : input,
  13132. _f : format,
  13133. _strict : strict
  13134. }).utc();
  13135. return m;
  13136. };
  13137. // creating with unix timestamp (in seconds)
  13138. moment.unix = function (input) {
  13139. return moment(input * 1000);
  13140. };
  13141. // duration
  13142. moment.duration = function (input, key) {
  13143. var duration = input,
  13144. // matching against regexp is expensive, do it on demand
  13145. match = null,
  13146. sign,
  13147. ret,
  13148. parseIso;
  13149. if (moment.isDuration(input)) {
  13150. duration = {
  13151. ms: input._milliseconds,
  13152. d: input._days,
  13153. M: input._months
  13154. };
  13155. } else if (typeof input === 'number') {
  13156. duration = {};
  13157. if (key) {
  13158. duration[key] = input;
  13159. } else {
  13160. duration.milliseconds = input;
  13161. }
  13162. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  13163. sign = (match[1] === "-") ? -1 : 1;
  13164. duration = {
  13165. y: 0,
  13166. d: toInt(match[DATE]) * sign,
  13167. h: toInt(match[HOUR]) * sign,
  13168. m: toInt(match[MINUTE]) * sign,
  13169. s: toInt(match[SECOND]) * sign,
  13170. ms: toInt(match[MILLISECOND]) * sign
  13171. };
  13172. } else if (!!(match = isoDurationRegex.exec(input))) {
  13173. sign = (match[1] === "-") ? -1 : 1;
  13174. parseIso = function (inp) {
  13175. // We'd normally use ~~inp for this, but unfortunately it also
  13176. // converts floats to ints.
  13177. // inp may be undefined, so careful calling replace on it.
  13178. var res = inp && parseFloat(inp.replace(',', '.'));
  13179. // apply sign while we're at it
  13180. return (isNaN(res) ? 0 : res) * sign;
  13181. };
  13182. duration = {
  13183. y: parseIso(match[2]),
  13184. M: parseIso(match[3]),
  13185. d: parseIso(match[4]),
  13186. h: parseIso(match[5]),
  13187. m: parseIso(match[6]),
  13188. s: parseIso(match[7]),
  13189. w: parseIso(match[8])
  13190. };
  13191. }
  13192. ret = new Duration(duration);
  13193. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  13194. ret._lang = input._lang;
  13195. }
  13196. return ret;
  13197. };
  13198. // version number
  13199. moment.version = VERSION;
  13200. // default format
  13201. moment.defaultFormat = isoFormat;
  13202. // This function will be called whenever a moment is mutated.
  13203. // It is intended to keep the offset in sync with the timezone.
  13204. moment.updateOffset = function () {};
  13205. // This function will load languages and then set the global language. If
  13206. // no arguments are passed in, it will simply return the current global
  13207. // language key.
  13208. moment.lang = function (key, values) {
  13209. var r;
  13210. if (!key) {
  13211. return moment.fn._lang._abbr;
  13212. }
  13213. if (values) {
  13214. loadLang(normalizeLanguage(key), values);
  13215. } else if (values === null) {
  13216. unloadLang(key);
  13217. key = 'en';
  13218. } else if (!languages[key]) {
  13219. getLangDefinition(key);
  13220. }
  13221. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  13222. return r._abbr;
  13223. };
  13224. // returns language data
  13225. moment.langData = function (key) {
  13226. if (key && key._lang && key._lang._abbr) {
  13227. key = key._lang._abbr;
  13228. }
  13229. return getLangDefinition(key);
  13230. };
  13231. // compare moment object
  13232. moment.isMoment = function (obj) {
  13233. return obj instanceof Moment;
  13234. };
  13235. // for typechecking Duration objects
  13236. moment.isDuration = function (obj) {
  13237. return obj instanceof Duration;
  13238. };
  13239. for (i = lists.length - 1; i >= 0; --i) {
  13240. makeList(lists[i]);
  13241. }
  13242. moment.normalizeUnits = function (units) {
  13243. return normalizeUnits(units);
  13244. };
  13245. moment.invalid = function (flags) {
  13246. var m = moment.utc(NaN);
  13247. if (flags != null) {
  13248. extend(m._pf, flags);
  13249. }
  13250. else {
  13251. m._pf.userInvalidated = true;
  13252. }
  13253. return m;
  13254. };
  13255. moment.parseZone = function (input) {
  13256. return moment(input).parseZone();
  13257. };
  13258. /************************************
  13259. Moment Prototype
  13260. ************************************/
  13261. extend(moment.fn = Moment.prototype, {
  13262. clone : function () {
  13263. return moment(this);
  13264. },
  13265. valueOf : function () {
  13266. return +this._d + ((this._offset || 0) * 60000);
  13267. },
  13268. unix : function () {
  13269. return Math.floor(+this / 1000);
  13270. },
  13271. toString : function () {
  13272. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  13273. },
  13274. toDate : function () {
  13275. return this._offset ? new Date(+this) : this._d;
  13276. },
  13277. toISOString : function () {
  13278. var m = moment(this).utc();
  13279. if (0 < m.year() && m.year() <= 9999) {
  13280. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  13281. } else {
  13282. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  13283. }
  13284. },
  13285. toArray : function () {
  13286. var m = this;
  13287. return [
  13288. m.year(),
  13289. m.month(),
  13290. m.date(),
  13291. m.hours(),
  13292. m.minutes(),
  13293. m.seconds(),
  13294. m.milliseconds()
  13295. ];
  13296. },
  13297. isValid : function () {
  13298. return isValid(this);
  13299. },
  13300. isDSTShifted : function () {
  13301. if (this._a) {
  13302. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  13303. }
  13304. return false;
  13305. },
  13306. parsingFlags : function () {
  13307. return extend({}, this._pf);
  13308. },
  13309. invalidAt: function () {
  13310. return this._pf.overflow;
  13311. },
  13312. utc : function () {
  13313. return this.zone(0);
  13314. },
  13315. local : function () {
  13316. this.zone(0);
  13317. this._isUTC = false;
  13318. return this;
  13319. },
  13320. format : function (inputString) {
  13321. var output = formatMoment(this, inputString || moment.defaultFormat);
  13322. return this.lang().postformat(output);
  13323. },
  13324. add : function (input, val) {
  13325. var dur;
  13326. // switch args to support add('s', 1) and add(1, 's')
  13327. if (typeof input === 'string') {
  13328. dur = moment.duration(+val, input);
  13329. } else {
  13330. dur = moment.duration(input, val);
  13331. }
  13332. addOrSubtractDurationFromMoment(this, dur, 1);
  13333. return this;
  13334. },
  13335. subtract : function (input, val) {
  13336. var dur;
  13337. // switch args to support subtract('s', 1) and subtract(1, 's')
  13338. if (typeof input === 'string') {
  13339. dur = moment.duration(+val, input);
  13340. } else {
  13341. dur = moment.duration(input, val);
  13342. }
  13343. addOrSubtractDurationFromMoment(this, dur, -1);
  13344. return this;
  13345. },
  13346. diff : function (input, units, asFloat) {
  13347. var that = makeAs(input, this),
  13348. zoneDiff = (this.zone() - that.zone()) * 6e4,
  13349. diff, output;
  13350. units = normalizeUnits(units);
  13351. if (units === 'year' || units === 'month') {
  13352. // average number of days in the months in the given dates
  13353. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  13354. // difference in months
  13355. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  13356. // adjust by taking difference in days, average number of days
  13357. // and dst in the given months.
  13358. output += ((this - moment(this).startOf('month')) -
  13359. (that - moment(that).startOf('month'))) / diff;
  13360. // same as above but with zones, to negate all dst
  13361. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  13362. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  13363. if (units === 'year') {
  13364. output = output / 12;
  13365. }
  13366. } else {
  13367. diff = (this - that);
  13368. output = units === 'second' ? diff / 1e3 : // 1000
  13369. units === 'minute' ? diff / 6e4 : // 1000 * 60
  13370. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  13371. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  13372. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  13373. diff;
  13374. }
  13375. return asFloat ? output : absRound(output);
  13376. },
  13377. from : function (time, withoutSuffix) {
  13378. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  13379. },
  13380. fromNow : function (withoutSuffix) {
  13381. return this.from(moment(), withoutSuffix);
  13382. },
  13383. calendar : function () {
  13384. // We want to compare the start of today, vs this.
  13385. // Getting start-of-today depends on whether we're zone'd or not.
  13386. var sod = makeAs(moment(), this).startOf('day'),
  13387. diff = this.diff(sod, 'days', true),
  13388. format = diff < -6 ? 'sameElse' :
  13389. diff < -1 ? 'lastWeek' :
  13390. diff < 0 ? 'lastDay' :
  13391. diff < 1 ? 'sameDay' :
  13392. diff < 2 ? 'nextDay' :
  13393. diff < 7 ? 'nextWeek' : 'sameElse';
  13394. return this.format(this.lang().calendar(format, this));
  13395. },
  13396. isLeapYear : function () {
  13397. return isLeapYear(this.year());
  13398. },
  13399. isDST : function () {
  13400. return (this.zone() < this.clone().month(0).zone() ||
  13401. this.zone() < this.clone().month(5).zone());
  13402. },
  13403. day : function (input) {
  13404. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  13405. if (input != null) {
  13406. input = parseWeekday(input, this.lang());
  13407. return this.add({ d : input - day });
  13408. } else {
  13409. return day;
  13410. }
  13411. },
  13412. month : function (input) {
  13413. var utc = this._isUTC ? 'UTC' : '',
  13414. dayOfMonth;
  13415. if (input != null) {
  13416. if (typeof input === 'string') {
  13417. input = this.lang().monthsParse(input);
  13418. if (typeof input !== 'number') {
  13419. return this;
  13420. }
  13421. }
  13422. dayOfMonth = this.date();
  13423. this.date(1);
  13424. this._d['set' + utc + 'Month'](input);
  13425. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  13426. moment.updateOffset(this);
  13427. return this;
  13428. } else {
  13429. return this._d['get' + utc + 'Month']();
  13430. }
  13431. },
  13432. startOf: function (units) {
  13433. units = normalizeUnits(units);
  13434. // the following switch intentionally omits break keywords
  13435. // to utilize falling through the cases.
  13436. switch (units) {
  13437. case 'year':
  13438. this.month(0);
  13439. /* falls through */
  13440. case 'month':
  13441. this.date(1);
  13442. /* falls through */
  13443. case 'week':
  13444. case 'isoWeek':
  13445. case 'day':
  13446. this.hours(0);
  13447. /* falls through */
  13448. case 'hour':
  13449. this.minutes(0);
  13450. /* falls through */
  13451. case 'minute':
  13452. this.seconds(0);
  13453. /* falls through */
  13454. case 'second':
  13455. this.milliseconds(0);
  13456. /* falls through */
  13457. }
  13458. // weeks are a special case
  13459. if (units === 'week') {
  13460. this.weekday(0);
  13461. } else if (units === 'isoWeek') {
  13462. this.isoWeekday(1);
  13463. }
  13464. return this;
  13465. },
  13466. endOf: function (units) {
  13467. units = normalizeUnits(units);
  13468. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  13469. },
  13470. isAfter: function (input, units) {
  13471. units = typeof units !== 'undefined' ? units : 'millisecond';
  13472. return +this.clone().startOf(units) > +moment(input).startOf(units);
  13473. },
  13474. isBefore: function (input, units) {
  13475. units = typeof units !== 'undefined' ? units : 'millisecond';
  13476. return +this.clone().startOf(units) < +moment(input).startOf(units);
  13477. },
  13478. isSame: function (input, units) {
  13479. units = units || 'ms';
  13480. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  13481. },
  13482. min: function (other) {
  13483. other = moment.apply(null, arguments);
  13484. return other < this ? this : other;
  13485. },
  13486. max: function (other) {
  13487. other = moment.apply(null, arguments);
  13488. return other > this ? this : other;
  13489. },
  13490. zone : function (input) {
  13491. var offset = this._offset || 0;
  13492. if (input != null) {
  13493. if (typeof input === "string") {
  13494. input = timezoneMinutesFromString(input);
  13495. }
  13496. if (Math.abs(input) < 16) {
  13497. input = input * 60;
  13498. }
  13499. this._offset = input;
  13500. this._isUTC = true;
  13501. if (offset !== input) {
  13502. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  13503. }
  13504. } else {
  13505. return this._isUTC ? offset : this._d.getTimezoneOffset();
  13506. }
  13507. return this;
  13508. },
  13509. zoneAbbr : function () {
  13510. return this._isUTC ? "UTC" : "";
  13511. },
  13512. zoneName : function () {
  13513. return this._isUTC ? "Coordinated Universal Time" : "";
  13514. },
  13515. parseZone : function () {
  13516. if (this._tzm) {
  13517. this.zone(this._tzm);
  13518. } else if (typeof this._i === 'string') {
  13519. this.zone(this._i);
  13520. }
  13521. return this;
  13522. },
  13523. hasAlignedHourOffset : function (input) {
  13524. if (!input) {
  13525. input = 0;
  13526. }
  13527. else {
  13528. input = moment(input).zone();
  13529. }
  13530. return (this.zone() - input) % 60 === 0;
  13531. },
  13532. daysInMonth : function () {
  13533. return daysInMonth(this.year(), this.month());
  13534. },
  13535. dayOfYear : function (input) {
  13536. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  13537. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  13538. },
  13539. quarter : function () {
  13540. return Math.ceil((this.month() + 1.0) / 3.0);
  13541. },
  13542. weekYear : function (input) {
  13543. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  13544. return input == null ? year : this.add("y", (input - year));
  13545. },
  13546. isoWeekYear : function (input) {
  13547. var year = weekOfYear(this, 1, 4).year;
  13548. return input == null ? year : this.add("y", (input - year));
  13549. },
  13550. week : function (input) {
  13551. var week = this.lang().week(this);
  13552. return input == null ? week : this.add("d", (input - week) * 7);
  13553. },
  13554. isoWeek : function (input) {
  13555. var week = weekOfYear(this, 1, 4).week;
  13556. return input == null ? week : this.add("d", (input - week) * 7);
  13557. },
  13558. weekday : function (input) {
  13559. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  13560. return input == null ? weekday : this.add("d", input - weekday);
  13561. },
  13562. isoWeekday : function (input) {
  13563. // behaves the same as moment#day except
  13564. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  13565. // as a setter, sunday should belong to the previous week.
  13566. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  13567. },
  13568. get : function (units) {
  13569. units = normalizeUnits(units);
  13570. return this[units]();
  13571. },
  13572. set : function (units, value) {
  13573. units = normalizeUnits(units);
  13574. if (typeof this[units] === 'function') {
  13575. this[units](value);
  13576. }
  13577. return this;
  13578. },
  13579. // If passed a language key, it will set the language for this
  13580. // instance. Otherwise, it will return the language configuration
  13581. // variables for this instance.
  13582. lang : function (key) {
  13583. if (key === undefined) {
  13584. return this._lang;
  13585. } else {
  13586. this._lang = getLangDefinition(key);
  13587. return this;
  13588. }
  13589. }
  13590. });
  13591. // helper for adding shortcuts
  13592. function makeGetterAndSetter(name, key) {
  13593. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  13594. var utc = this._isUTC ? 'UTC' : '';
  13595. if (input != null) {
  13596. this._d['set' + utc + key](input);
  13597. moment.updateOffset(this);
  13598. return this;
  13599. } else {
  13600. return this._d['get' + utc + key]();
  13601. }
  13602. };
  13603. }
  13604. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  13605. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  13606. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  13607. }
  13608. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  13609. makeGetterAndSetter('year', 'FullYear');
  13610. // add plural methods
  13611. moment.fn.days = moment.fn.day;
  13612. moment.fn.months = moment.fn.month;
  13613. moment.fn.weeks = moment.fn.week;
  13614. moment.fn.isoWeeks = moment.fn.isoWeek;
  13615. // add aliased format methods
  13616. moment.fn.toJSON = moment.fn.toISOString;
  13617. /************************************
  13618. Duration Prototype
  13619. ************************************/
  13620. extend(moment.duration.fn = Duration.prototype, {
  13621. _bubble : function () {
  13622. var milliseconds = this._milliseconds,
  13623. days = this._days,
  13624. months = this._months,
  13625. data = this._data,
  13626. seconds, minutes, hours, years;
  13627. // The following code bubbles up values, see the tests for
  13628. // examples of what that means.
  13629. data.milliseconds = milliseconds % 1000;
  13630. seconds = absRound(milliseconds / 1000);
  13631. data.seconds = seconds % 60;
  13632. minutes = absRound(seconds / 60);
  13633. data.minutes = minutes % 60;
  13634. hours = absRound(minutes / 60);
  13635. data.hours = hours % 24;
  13636. days += absRound(hours / 24);
  13637. data.days = days % 30;
  13638. months += absRound(days / 30);
  13639. data.months = months % 12;
  13640. years = absRound(months / 12);
  13641. data.years = years;
  13642. },
  13643. weeks : function () {
  13644. return absRound(this.days() / 7);
  13645. },
  13646. valueOf : function () {
  13647. return this._milliseconds +
  13648. this._days * 864e5 +
  13649. (this._months % 12) * 2592e6 +
  13650. toInt(this._months / 12) * 31536e6;
  13651. },
  13652. humanize : function (withSuffix) {
  13653. var difference = +this,
  13654. output = relativeTime(difference, !withSuffix, this.lang());
  13655. if (withSuffix) {
  13656. output = this.lang().pastFuture(difference, output);
  13657. }
  13658. return this.lang().postformat(output);
  13659. },
  13660. add : function (input, val) {
  13661. // supports only 2.0-style add(1, 's') or add(moment)
  13662. var dur = moment.duration(input, val);
  13663. this._milliseconds += dur._milliseconds;
  13664. this._days += dur._days;
  13665. this._months += dur._months;
  13666. this._bubble();
  13667. return this;
  13668. },
  13669. subtract : function (input, val) {
  13670. var dur = moment.duration(input, val);
  13671. this._milliseconds -= dur._milliseconds;
  13672. this._days -= dur._days;
  13673. this._months -= dur._months;
  13674. this._bubble();
  13675. return this;
  13676. },
  13677. get : function (units) {
  13678. units = normalizeUnits(units);
  13679. return this[units.toLowerCase() + 's']();
  13680. },
  13681. as : function (units) {
  13682. units = normalizeUnits(units);
  13683. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  13684. },
  13685. lang : moment.fn.lang,
  13686. toIsoString : function () {
  13687. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  13688. var years = Math.abs(this.years()),
  13689. months = Math.abs(this.months()),
  13690. days = Math.abs(this.days()),
  13691. hours = Math.abs(this.hours()),
  13692. minutes = Math.abs(this.minutes()),
  13693. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  13694. if (!this.asSeconds()) {
  13695. // this is the same as C#'s (Noda) and python (isodate)...
  13696. // but not other JS (goog.date)
  13697. return 'P0D';
  13698. }
  13699. return (this.asSeconds() < 0 ? '-' : '') +
  13700. 'P' +
  13701. (years ? years + 'Y' : '') +
  13702. (months ? months + 'M' : '') +
  13703. (days ? days + 'D' : '') +
  13704. ((hours || minutes || seconds) ? 'T' : '') +
  13705. (hours ? hours + 'H' : '') +
  13706. (minutes ? minutes + 'M' : '') +
  13707. (seconds ? seconds + 'S' : '');
  13708. }
  13709. });
  13710. function makeDurationGetter(name) {
  13711. moment.duration.fn[name] = function () {
  13712. return this._data[name];
  13713. };
  13714. }
  13715. function makeDurationAsGetter(name, factor) {
  13716. moment.duration.fn['as' + name] = function () {
  13717. return +this / factor;
  13718. };
  13719. }
  13720. for (i in unitMillisecondFactors) {
  13721. if (unitMillisecondFactors.hasOwnProperty(i)) {
  13722. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  13723. makeDurationGetter(i.toLowerCase());
  13724. }
  13725. }
  13726. makeDurationAsGetter('Weeks', 6048e5);
  13727. moment.duration.fn.asMonths = function () {
  13728. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  13729. };
  13730. /************************************
  13731. Default Lang
  13732. ************************************/
  13733. // Set default language, other languages will inherit from English.
  13734. moment.lang('en', {
  13735. ordinal : function (number) {
  13736. var b = number % 10,
  13737. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  13738. (b === 1) ? 'st' :
  13739. (b === 2) ? 'nd' :
  13740. (b === 3) ? 'rd' : 'th';
  13741. return number + output;
  13742. }
  13743. });
  13744. /* EMBED_LANGUAGES */
  13745. /************************************
  13746. Exposing Moment
  13747. ************************************/
  13748. function makeGlobal(deprecate) {
  13749. var warned = false, local_moment = moment;
  13750. /*global ender:false */
  13751. if (typeof ender !== 'undefined') {
  13752. return;
  13753. }
  13754. // here, `this` means `window` in the browser, or `global` on the server
  13755. // add `moment` as a global object via a string identifier,
  13756. // for Closure Compiler "advanced" mode
  13757. if (deprecate) {
  13758. global.moment = function () {
  13759. if (!warned && console && console.warn) {
  13760. warned = true;
  13761. console.warn(
  13762. "Accessing Moment through the global scope is " +
  13763. "deprecated, and will be removed in an upcoming " +
  13764. "release.");
  13765. }
  13766. return local_moment.apply(null, arguments);
  13767. };
  13768. extend(global.moment, local_moment);
  13769. } else {
  13770. global['moment'] = moment;
  13771. }
  13772. }
  13773. // CommonJS module is defined
  13774. if (hasModule) {
  13775. module.exports = moment;
  13776. makeGlobal(true);
  13777. } else if (typeof define === "function" && define.amd) {
  13778. define("moment", function (require, exports, module) {
  13779. if (module.config && module.config() && module.config().noGlobal !== true) {
  13780. // If user provided noGlobal, he is aware of global
  13781. makeGlobal(module.config().noGlobal === undefined);
  13782. }
  13783. return moment;
  13784. });
  13785. } else {
  13786. makeGlobal();
  13787. }
  13788. }).call(this);
  13789. },{}]},{},[1])
  13790. (1)
  13791. });