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.

7099 lines
216 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.0.8
  8. * @date 2013-05-08
  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("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports;
  26. return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
  27. /**
  28. * vis.js module imports
  29. */
  30. var moment = require('moment');
  31. /**
  32. * utility functions
  33. */
  34. var util = {};
  35. /**
  36. * Test whether given object is a number
  37. * @param {*} object
  38. * @return {Boolean} isNumber
  39. */
  40. util.isNumber = function isNumber(object) {
  41. return (object instanceof Number || typeof object == 'number');
  42. };
  43. /**
  44. * Test whether given object is a string
  45. * @param {*} object
  46. * @return {Boolean} isString
  47. */
  48. util.isString = function isString(object) {
  49. return (object instanceof String || typeof object == 'string');
  50. };
  51. /**
  52. * Test whether given object is a Date, or a String containing a Date
  53. * @param {Date | String} object
  54. * @return {Boolean} isDate
  55. */
  56. util.isDate = function isDate(object) {
  57. if (object instanceof Date) {
  58. return true;
  59. }
  60. else if (util.isString(object)) {
  61. // test whether this string contains a date
  62. var match = ASPDateRegex.exec(object);
  63. if (match) {
  64. return true;
  65. }
  66. else if (!isNaN(Date.parse(object))) {
  67. return true;
  68. }
  69. }
  70. return false;
  71. };
  72. /**
  73. * Test whether given object is an instance of google.visualization.DataTable
  74. * @param {*} object
  75. * @return {Boolean} isDataTable
  76. */
  77. util.isDataTable = function isDataTable(object) {
  78. return (typeof (google) !== 'undefined') &&
  79. (google.visualization) &&
  80. (google.visualization.DataTable) &&
  81. (object instanceof google.visualization.DataTable);
  82. };
  83. /**
  84. * Create a semi UUID
  85. * source: http://stackoverflow.com/a/105074/1262753
  86. * @return {String} uuid
  87. */
  88. util.randomUUID = function randomUUID () {
  89. var S4 = function () {
  90. return Math.floor(
  91. Math.random() * 0x10000 /* 65536 */
  92. ).toString(16);
  93. };
  94. return (
  95. S4() + S4() + '-' +
  96. S4() + '-' +
  97. S4() + '-' +
  98. S4() + '-' +
  99. S4() + S4() + S4()
  100. );
  101. };
  102. /**
  103. * Extend object a with the properties of object b
  104. * @param {Object} a
  105. * @param {Object} b
  106. * @return {Object} a
  107. */
  108. util.extend = function (a, b) {
  109. for (var prop in b) {
  110. if (b.hasOwnProperty(prop)) {
  111. a[prop] = b[prop];
  112. }
  113. }
  114. return a;
  115. };
  116. /**
  117. * Cast an object to another type
  118. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  119. * @param {String | undefined} type Name of the type. Available types:
  120. * 'Boolean', 'Number', 'String',
  121. * 'Date', 'Moment', ISODate', 'ASPDate'.
  122. * @return {*} object
  123. * @throws Error
  124. */
  125. util.cast = function cast(object, type) {
  126. var match;
  127. if (object === undefined) {
  128. return undefined;
  129. }
  130. if (object === null) {
  131. return null;
  132. }
  133. if (!type) {
  134. return object;
  135. }
  136. if (!(typeof type === 'string') && !(type instanceof String)) {
  137. throw new Error('Type must be a string');
  138. }
  139. //noinspection FallthroughInSwitchStatementJS
  140. switch (type) {
  141. case 'boolean':
  142. case 'Boolean':
  143. return Boolean(object);
  144. case 'number':
  145. case 'Number':
  146. return Number(object);
  147. case 'string':
  148. case 'String':
  149. return String(object);
  150. case 'Date':
  151. if (util.isNumber(object)) {
  152. return new Date(object);
  153. }
  154. if (object instanceof Date) {
  155. return new Date(object.valueOf());
  156. }
  157. else if (moment.isMoment(object)) {
  158. return new Date(object.valueOf());
  159. }
  160. if (util.isString(object)) {
  161. // parse ASP.Net Date pattern,
  162. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  163. // code from http://momentjs.com/
  164. match = ASPDateRegex.exec(object);
  165. if (match) {
  166. return new Date(Number(match[1])); // parse number
  167. }
  168. else {
  169. return moment(object).toDate(); // parse string
  170. }
  171. }
  172. else {
  173. throw new Error(
  174. 'Cannot cast object of type ' + util.getType(object) +
  175. ' to type Date');
  176. }
  177. case 'Moment':
  178. if (util.isNumber(object)) {
  179. return moment(object);
  180. }
  181. if (object instanceof Date) {
  182. return moment(object.valueOf());
  183. }
  184. else if (moment.isMoment(object)) {
  185. return moment.clone();
  186. }
  187. if (util.isString(object)) {
  188. // parse ASP.Net Date pattern,
  189. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  190. // code from http://momentjs.com/
  191. match = ASPDateRegex.exec(object);
  192. if (match) {
  193. return moment(Number(match[1])); // parse number
  194. }
  195. else {
  196. return moment(object); // parse string
  197. }
  198. }
  199. else {
  200. throw new Error(
  201. 'Cannot cast object of type ' + util.getType(object) +
  202. ' to type Date');
  203. }
  204. case 'ISODate':
  205. if (object instanceof Date) {
  206. return object.toISOString();
  207. }
  208. else if (moment.isMoment(object)) {
  209. return object.toDate().toISOString();
  210. }
  211. else if (util.isNumber(object) || util.isString(object)) {
  212. return moment(object).toDate().toISOString();
  213. }
  214. else {
  215. throw new Error(
  216. 'Cannot cast object of type ' + util.getType(object) +
  217. ' to type ISODate');
  218. }
  219. case 'ASPDate':
  220. if (object instanceof Date) {
  221. return '/Date(' + object.valueOf() + ')/';
  222. }
  223. else if (util.isNumber(object) || util.isString(object)) {
  224. return '/Date(' + moment(object).valueOf() + ')/';
  225. }
  226. else {
  227. throw new Error(
  228. 'Cannot cast object of type ' + util.getType(object) +
  229. ' to type ASPDate');
  230. }
  231. default:
  232. throw new Error('Cannot cast object of type ' + util.getType(object) +
  233. ' to type "' + type + '"');
  234. }
  235. };
  236. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  237. /**
  238. * Get the type of an object, for example util.getType([]) returns 'Array'
  239. * @param {*} object
  240. * @return {String} type
  241. */
  242. util.getType = function getType(object) {
  243. var type = typeof object;
  244. if (type == 'object') {
  245. if (object == null) {
  246. return 'null';
  247. }
  248. if (object instanceof Boolean) {
  249. return 'Boolean';
  250. }
  251. if (object instanceof Number) {
  252. return 'Number';
  253. }
  254. if (object instanceof String) {
  255. return 'String';
  256. }
  257. if (object instanceof Array) {
  258. return 'Array';
  259. }
  260. if (object instanceof Date) {
  261. return 'Date';
  262. }
  263. return 'Object';
  264. }
  265. else if (type == 'number') {
  266. return 'Number';
  267. }
  268. else if (type == 'boolean') {
  269. return 'Boolean';
  270. }
  271. else if (type == 'string') {
  272. return 'String';
  273. }
  274. return type;
  275. };
  276. /**
  277. * Retrieve the absolute left value of a DOM element
  278. * @param {Element} elem A dom element, for example a div
  279. * @return {number} left The absolute left position of this element
  280. * in the browser page.
  281. */
  282. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  283. var doc = document.documentElement;
  284. var body = document.body;
  285. var left = elem.offsetLeft;
  286. var e = elem.offsetParent;
  287. while (e != null && e != body && e != doc) {
  288. left += e.offsetLeft;
  289. left -= e.scrollLeft;
  290. e = e.offsetParent;
  291. }
  292. return left;
  293. };
  294. /**
  295. * Retrieve the absolute top value of a DOM element
  296. * @param {Element} elem A dom element, for example a div
  297. * @return {number} top The absolute top position of this element
  298. * in the browser page.
  299. */
  300. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  301. var doc = document.documentElement;
  302. var body = document.body;
  303. var top = elem.offsetTop;
  304. var e = elem.offsetParent;
  305. while (e != null && e != body && e != doc) {
  306. top += e.offsetTop;
  307. top -= e.scrollTop;
  308. e = e.offsetParent;
  309. }
  310. return top;
  311. };
  312. /**
  313. * Get the absolute, vertical mouse position from an event.
  314. * @param {Event} event
  315. * @return {Number} pageY
  316. */
  317. util.getPageY = function getPageY (event) {
  318. if ('pageY' in event) {
  319. return event.pageY;
  320. }
  321. else {
  322. var clientY;
  323. if (('targetTouches' in event) && event.targetTouches.length) {
  324. clientY = event.targetTouches[0].clientY;
  325. }
  326. else {
  327. clientY = event.clientY;
  328. }
  329. var doc = document.documentElement;
  330. var body = document.body;
  331. return clientY +
  332. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  333. ( doc && doc.clientTop || body && body.clientTop || 0 );
  334. }
  335. };
  336. /**
  337. * Get the absolute, horizontal mouse position from an event.
  338. * @param {Event} event
  339. * @return {Number} pageX
  340. */
  341. util.getPageX = function getPageX (event) {
  342. if ('pageY' in event) {
  343. return event.pageX;
  344. }
  345. else {
  346. var clientX;
  347. if (('targetTouches' in event) && event.targetTouches.length) {
  348. clientX = event.targetTouches[0].clientX;
  349. }
  350. else {
  351. clientX = event.clientX;
  352. }
  353. var doc = document.documentElement;
  354. var body = document.body;
  355. return clientX +
  356. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  357. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  358. }
  359. };
  360. /**
  361. * add a className to the given elements style
  362. * @param {Element} elem
  363. * @param {String} className
  364. */
  365. util.addClassName = function addClassName(elem, className) {
  366. var classes = elem.className.split(' ');
  367. if (classes.indexOf(className) == -1) {
  368. classes.push(className); // add the class to the array
  369. elem.className = classes.join(' ');
  370. }
  371. };
  372. /**
  373. * add a className to the given elements style
  374. * @param {Element} elem
  375. * @param {String} className
  376. */
  377. util.removeClassName = function removeClassname(elem, className) {
  378. var classes = elem.className.split(' ');
  379. var index = classes.indexOf(className);
  380. if (index != -1) {
  381. classes.splice(index, 1); // remove the class from the array
  382. elem.className = classes.join(' ');
  383. }
  384. };
  385. /**
  386. * For each method for both arrays and objects.
  387. * In case of an array, the built-in Array.forEach() is applied.
  388. * In case of an Object, the method loops over all properties of the object.
  389. * @param {Object | Array} object An Object or Array
  390. * @param {function} callback Callback method, called for each item in
  391. * the object or array with three parameters:
  392. * callback(value, index, object)
  393. */
  394. util.forEach = function forEach (object, callback) {
  395. var i,
  396. len;
  397. if (object instanceof Array) {
  398. // array
  399. for (i = 0, len = object.length; i < len; i++) {
  400. callback(object[i], i, object);
  401. }
  402. }
  403. else {
  404. // object
  405. for (i in object) {
  406. if (object.hasOwnProperty(i)) {
  407. callback(object[i], i, object);
  408. }
  409. }
  410. }
  411. };
  412. /**
  413. * Update a property in an object
  414. * @param {Object} object
  415. * @param {String} key
  416. * @param {*} value
  417. * @return {Boolean} changed
  418. */
  419. util.updateProperty = function updateProp (object, key, value) {
  420. if (object[key] !== value) {
  421. object[key] = value;
  422. return true;
  423. }
  424. else {
  425. return false;
  426. }
  427. };
  428. /**
  429. * Add and event listener. Works for all browsers
  430. * @param {Element} element An html element
  431. * @param {string} action The action, for example "click",
  432. * without the prefix "on"
  433. * @param {function} listener The callback function to be executed
  434. * @param {boolean} [useCapture]
  435. */
  436. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  437. if (element.addEventListener) {
  438. if (useCapture === undefined)
  439. useCapture = false;
  440. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  441. action = "DOMMouseScroll"; // For Firefox
  442. }
  443. element.addEventListener(action, listener, useCapture);
  444. } else {
  445. element.attachEvent("on" + action, listener); // IE browsers
  446. }
  447. };
  448. /**
  449. * Remove an event listener from an element
  450. * @param {Element} element An html dom element
  451. * @param {string} action The name of the event, for example "mousedown"
  452. * @param {function} listener The listener function
  453. * @param {boolean} [useCapture]
  454. */
  455. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  456. if (element.removeEventListener) {
  457. // non-IE browsers
  458. if (useCapture === undefined)
  459. useCapture = false;
  460. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  461. action = "DOMMouseScroll"; // For Firefox
  462. }
  463. element.removeEventListener(action, listener, useCapture);
  464. } else {
  465. // IE browsers
  466. element.detachEvent("on" + action, listener);
  467. }
  468. };
  469. /**
  470. * Get HTML element which is the target of the event
  471. * @param {Event} event
  472. * @return {Element} target element
  473. */
  474. util.getTarget = function getTarget(event) {
  475. // code from http://www.quirksmode.org/js/events_properties.html
  476. if (!event) {
  477. event = window.event;
  478. }
  479. var target;
  480. if (event.target) {
  481. target = event.target;
  482. }
  483. else if (event.srcElement) {
  484. target = event.srcElement;
  485. }
  486. if (target.nodeType != undefined && target.nodeType == 3) {
  487. // defeat Safari bug
  488. target = target.parentNode;
  489. }
  490. return target;
  491. };
  492. /**
  493. * Stop event propagation
  494. */
  495. util.stopPropagation = function stopPropagation(event) {
  496. if (!event)
  497. event = window.event;
  498. if (event.stopPropagation) {
  499. event.stopPropagation(); // non-IE browsers
  500. }
  501. else {
  502. event.cancelBubble = true; // IE browsers
  503. }
  504. };
  505. /**
  506. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  507. */
  508. util.preventDefault = function preventDefault (event) {
  509. if (!event)
  510. event = window.event;
  511. if (event.preventDefault) {
  512. event.preventDefault(); // non-IE browsers
  513. }
  514. else {
  515. event.returnValue = false; // IE browsers
  516. }
  517. };
  518. util.option = {};
  519. /**
  520. * Cast a value as boolean
  521. * @param {Boolean | function | undefined} value
  522. * @param {Boolean} [defaultValue]
  523. * @returns {Boolean} bool
  524. */
  525. util.option.asBoolean = function (value, defaultValue) {
  526. if (typeof value == 'function') {
  527. value = value();
  528. }
  529. if (value != null) {
  530. return (value != false);
  531. }
  532. return defaultValue || null;
  533. };
  534. /**
  535. * Cast a value as number
  536. * @param {Boolean | function | undefined} value
  537. * @param {Number} [defaultValue]
  538. * @returns {Number} number
  539. */
  540. util.option.asNumber = function (value, defaultValue) {
  541. if (typeof value == 'function') {
  542. value = value();
  543. }
  544. if (value != null) {
  545. return Number(value) || defaultValue || null;
  546. }
  547. return defaultValue || null;
  548. };
  549. /**
  550. * Cast a value as string
  551. * @param {String | function | undefined} value
  552. * @param {String} [defaultValue]
  553. * @returns {String} str
  554. */
  555. util.option.asString = function (value, defaultValue) {
  556. if (typeof value == 'function') {
  557. value = value();
  558. }
  559. if (value != null) {
  560. return String(value);
  561. }
  562. return defaultValue || null;
  563. };
  564. /**
  565. * Cast a size or location in pixels or a percentage
  566. * @param {String | Number | function | undefined} value
  567. * @param {String} [defaultValue]
  568. * @returns {String} size
  569. */
  570. util.option.asSize = function (value, defaultValue) {
  571. if (typeof value == 'function') {
  572. value = value();
  573. }
  574. if (util.isString(value)) {
  575. return value;
  576. }
  577. else if (util.isNumber(value)) {
  578. return value + 'px';
  579. }
  580. else {
  581. return defaultValue || null;
  582. }
  583. };
  584. /**
  585. * Cast a value as DOM element
  586. * @param {HTMLElement | function | undefined} value
  587. * @param {HTMLElement} [defaultValue]
  588. * @returns {HTMLElement | null} dom
  589. */
  590. util.option.asElement = function (value, defaultValue) {
  591. if (typeof value == 'function') {
  592. value = value();
  593. }
  594. return value || defaultValue || null;
  595. };
  596. /**
  597. * load css from text
  598. * @param {String} css Text containing css
  599. */
  600. util.loadCss = function (css) {
  601. if (typeof document === 'undefined') {
  602. return;
  603. }
  604. // get the script location, and built the css file name from the js file name
  605. // http://stackoverflow.com/a/2161748/1262753
  606. // var scripts = document.getElementsByTagName('script');
  607. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  608. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  609. // inject css
  610. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  611. var style = document.createElement('style');
  612. style.type = 'text/css';
  613. if (style.styleSheet){
  614. style.styleSheet.cssText = css;
  615. } else {
  616. style.appendChild(document.createTextNode(css));
  617. }
  618. document.getElementsByTagName('head')[0].appendChild(style);
  619. };
  620. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  621. // it here in that case.
  622. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  623. if(!Array.prototype.indexOf) {
  624. Array.prototype.indexOf = function(obj){
  625. for(var i = 0; i < this.length; i++){
  626. if(this[i] == obj){
  627. return i;
  628. }
  629. }
  630. return -1;
  631. };
  632. try {
  633. console.log("Warning: Ancient browser detected. Please update your browser");
  634. }
  635. catch (err) {
  636. }
  637. }
  638. // Internet Explorer 8 and older does not support Array.forEach, so we define
  639. // it here in that case.
  640. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  641. if (!Array.prototype.forEach) {
  642. Array.prototype.forEach = function(fn, scope) {
  643. for(var i = 0, len = this.length; i < len; ++i) {
  644. fn.call(scope || this, this[i], i, this);
  645. }
  646. }
  647. }
  648. // Internet Explorer 8 and older does not support Array.map, so we define it
  649. // here in that case.
  650. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  651. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  652. // Reference: http://es5.github.com/#x15.4.4.19
  653. if (!Array.prototype.map) {
  654. Array.prototype.map = function(callback, thisArg) {
  655. var T, A, k;
  656. if (this == null) {
  657. throw new TypeError(" this is null or not defined");
  658. }
  659. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  660. var O = Object(this);
  661. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  662. // 3. Let len be ToUint32(lenValue).
  663. var len = O.length >>> 0;
  664. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  665. // See: http://es5.github.com/#x9.11
  666. if (typeof callback !== "function") {
  667. throw new TypeError(callback + " is not a function");
  668. }
  669. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  670. if (thisArg) {
  671. T = thisArg;
  672. }
  673. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  674. // the standard built-in constructor with that name and len is the value of len.
  675. A = new Array(len);
  676. // 7. Let k be 0
  677. k = 0;
  678. // 8. Repeat, while k < len
  679. while(k < len) {
  680. var kValue, mappedValue;
  681. // a. Let Pk be ToString(k).
  682. // This is implicit for LHS operands of the in operator
  683. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  684. // This step can be combined with c
  685. // c. If kPresent is true, then
  686. if (k in O) {
  687. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  688. kValue = O[ k ];
  689. // ii. Let mappedValue be the result of calling the Call internal method of callback
  690. // with T as the this value and argument list containing kValue, k, and O.
  691. mappedValue = callback.call(T, kValue, k, O);
  692. // iii. Call the DefineOwnProperty internal method of A with arguments
  693. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  694. // and false.
  695. // In browsers that support Object.defineProperty, use the following:
  696. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  697. // For best browser support, use the following:
  698. A[ k ] = mappedValue;
  699. }
  700. // d. Increase k by 1.
  701. k++;
  702. }
  703. // 9. return A
  704. return A;
  705. };
  706. }
  707. // Internet Explorer 8 and older does not support Array.filter, so we define it
  708. // here in that case.
  709. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  710. if (!Array.prototype.filter) {
  711. Array.prototype.filter = function(fun /*, thisp */) {
  712. "use strict";
  713. if (this == null) {
  714. throw new TypeError();
  715. }
  716. var t = Object(this);
  717. var len = t.length >>> 0;
  718. if (typeof fun != "function") {
  719. throw new TypeError();
  720. }
  721. var res = [];
  722. var thisp = arguments[1];
  723. for (var i = 0; i < len; i++) {
  724. if (i in t) {
  725. var val = t[i]; // in case fun mutates this
  726. if (fun.call(thisp, val, i, t))
  727. res.push(val);
  728. }
  729. }
  730. return res;
  731. };
  732. }
  733. // Internet Explorer 8 and older does not support Object.keys, so we define it
  734. // here in that case.
  735. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  736. if (!Object.keys) {
  737. Object.keys = (function () {
  738. var hasOwnProperty = Object.prototype.hasOwnProperty,
  739. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  740. dontEnums = [
  741. 'toString',
  742. 'toLocaleString',
  743. 'valueOf',
  744. 'hasOwnProperty',
  745. 'isPrototypeOf',
  746. 'propertyIsEnumerable',
  747. 'constructor'
  748. ],
  749. dontEnumsLength = dontEnums.length;
  750. return function (obj) {
  751. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  752. throw new TypeError('Object.keys called on non-object');
  753. }
  754. var result = [];
  755. for (var prop in obj) {
  756. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  757. }
  758. if (hasDontEnumBug) {
  759. for (var i=0; i < dontEnumsLength; i++) {
  760. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  761. }
  762. }
  763. return result;
  764. }
  765. })()
  766. }
  767. // Internet Explorer 8 and older does not support Array.isArray,
  768. // so we define it here in that case.
  769. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  770. if(!Array.isArray) {
  771. Array.isArray = function (vArg) {
  772. return Object.prototype.toString.call(vArg) === "[object Array]";
  773. };
  774. }
  775. // Internet Explorer 8 and older does not support Function.bind,
  776. // so we define it here in that case.
  777. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  778. if (!Function.prototype.bind) {
  779. Function.prototype.bind = function (oThis) {
  780. if (typeof this !== "function") {
  781. // closest thing possible to the ECMAScript 5 internal IsCallable function
  782. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  783. }
  784. var aArgs = Array.prototype.slice.call(arguments, 1),
  785. fToBind = this,
  786. fNOP = function () {},
  787. fBound = function () {
  788. return fToBind.apply(this instanceof fNOP && oThis
  789. ? this
  790. : oThis,
  791. aArgs.concat(Array.prototype.slice.call(arguments)));
  792. };
  793. fNOP.prototype = this.prototype;
  794. fBound.prototype = new fNOP();
  795. return fBound;
  796. };
  797. }
  798. /**
  799. * Event listener (singleton)
  800. */
  801. // TODO: replace usage of the event listener for the EventBus
  802. var events = {
  803. 'listeners': [],
  804. /**
  805. * Find a single listener by its object
  806. * @param {Object} object
  807. * @return {Number} index -1 when not found
  808. */
  809. 'indexOf': function (object) {
  810. var listeners = this.listeners;
  811. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  812. var listener = listeners[i];
  813. if (listener && listener.object == object) {
  814. return i;
  815. }
  816. }
  817. return -1;
  818. },
  819. /**
  820. * Add an event listener
  821. * @param {Object} object
  822. * @param {String} event The name of an event, for example 'select'
  823. * @param {function} callback The callback method, called when the
  824. * event takes place
  825. */
  826. 'addListener': function (object, event, callback) {
  827. var index = this.indexOf(object);
  828. var listener = this.listeners[index];
  829. if (!listener) {
  830. listener = {
  831. 'object': object,
  832. 'events': {}
  833. };
  834. this.listeners.push(listener);
  835. }
  836. var callbacks = listener.events[event];
  837. if (!callbacks) {
  838. callbacks = [];
  839. listener.events[event] = callbacks;
  840. }
  841. // add the callback if it does not yet exist
  842. if (callbacks.indexOf(callback) == -1) {
  843. callbacks.push(callback);
  844. }
  845. },
  846. /**
  847. * Remove an event listener
  848. * @param {Object} object
  849. * @param {String} event The name of an event, for example 'select'
  850. * @param {function} callback The registered callback method
  851. */
  852. 'removeListener': function (object, event, callback) {
  853. var index = this.indexOf(object);
  854. var listener = this.listeners[index];
  855. if (listener) {
  856. var callbacks = listener.events[event];
  857. if (callbacks) {
  858. index = callbacks.indexOf(callback);
  859. if (index != -1) {
  860. callbacks.splice(index, 1);
  861. }
  862. // remove the array when empty
  863. if (callbacks.length == 0) {
  864. delete listener.events[event];
  865. }
  866. }
  867. // count the number of registered events. remove listener when empty
  868. var count = 0;
  869. var events = listener.events;
  870. for (var e in events) {
  871. if (events.hasOwnProperty(e)) {
  872. count++;
  873. }
  874. }
  875. if (count == 0) {
  876. delete this.listeners[index];
  877. }
  878. }
  879. },
  880. /**
  881. * Remove all registered event listeners
  882. */
  883. 'removeAllListeners': function () {
  884. this.listeners = [];
  885. },
  886. /**
  887. * Trigger an event. All registered event handlers will be called
  888. * @param {Object} object
  889. * @param {String} event
  890. * @param {Object} properties (optional)
  891. */
  892. 'trigger': function (object, event, properties) {
  893. var index = this.indexOf(object);
  894. var listener = this.listeners[index];
  895. if (listener) {
  896. var callbacks = listener.events[event];
  897. if (callbacks) {
  898. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  899. callbacks[i](properties);
  900. }
  901. }
  902. }
  903. }
  904. };
  905. /**
  906. * @constructor TimeStep
  907. * The class TimeStep is an iterator for dates. You provide a start date and an
  908. * end date. The class itself determines the best scale (step size) based on the
  909. * provided start Date, end Date, and minimumStep.
  910. *
  911. * If minimumStep is provided, the step size is chosen as close as possible
  912. * to the minimumStep but larger than minimumStep. If minimumStep is not
  913. * provided, the scale is set to 1 DAY.
  914. * The minimumStep should correspond with the onscreen size of about 6 characters
  915. *
  916. * Alternatively, you can set a scale by hand.
  917. * After creation, you can initialize the class by executing first(). Then you
  918. * can iterate from the start date to the end date via next(). You can check if
  919. * the end date is reached with the function hasNext(). After each step, you can
  920. * retrieve the current date via getCurrent().
  921. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  922. * days, to years.
  923. *
  924. * Version: 1.2
  925. *
  926. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  927. * or new Date(2010, 9, 21, 23, 45, 00)
  928. * @param {Date} [end] The end date
  929. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  930. */
  931. TimeStep = function(start, end, minimumStep) {
  932. // variables
  933. this.current = new Date();
  934. this._start = new Date();
  935. this._end = new Date();
  936. this.autoScale = true;
  937. this.scale = TimeStep.SCALE.DAY;
  938. this.step = 1;
  939. // initialize the range
  940. this.setRange(start, end, minimumStep);
  941. };
  942. /// enum scale
  943. TimeStep.SCALE = {
  944. MILLISECOND: 1,
  945. SECOND: 2,
  946. MINUTE: 3,
  947. HOUR: 4,
  948. DAY: 5,
  949. WEEKDAY: 6,
  950. MONTH: 7,
  951. YEAR: 8
  952. };
  953. /**
  954. * Set a new range
  955. * If minimumStep is provided, the step size is chosen as close as possible
  956. * to the minimumStep but larger than minimumStep. If minimumStep is not
  957. * provided, the scale is set to 1 DAY.
  958. * The minimumStep should correspond with the onscreen size of about 6 characters
  959. * @param {Date} [start] The start date and time.
  960. * @param {Date} [end] The end date and time.
  961. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  962. */
  963. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  964. if (!(start instanceof Date) || !(end instanceof Date)) {
  965. //throw "No legal start or end date in method setRange";
  966. return;
  967. }
  968. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  969. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  970. if (this.autoScale) {
  971. this.setMinimumStep(minimumStep);
  972. }
  973. };
  974. /**
  975. * Set the range iterator to the start date.
  976. */
  977. TimeStep.prototype.first = function() {
  978. this.current = new Date(this._start.valueOf());
  979. this.roundToMinor();
  980. };
  981. /**
  982. * Round the current date to the first minor date value
  983. * This must be executed once when the current date is set to start Date
  984. */
  985. TimeStep.prototype.roundToMinor = function() {
  986. // round to floor
  987. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  988. //noinspection FallthroughInSwitchStatementJS
  989. switch (this.scale) {
  990. case TimeStep.SCALE.YEAR:
  991. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  992. this.current.setMonth(0);
  993. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  994. case TimeStep.SCALE.DAY: // intentional fall through
  995. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  996. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  997. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  998. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  999. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  1000. }
  1001. if (this.step != 1) {
  1002. // round down to the first minor value that is a multiple of the current step size
  1003. switch (this.scale) {
  1004. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  1005. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  1006. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  1007. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  1008. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1009. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  1010. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  1011. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  1012. default: break;
  1013. }
  1014. }
  1015. };
  1016. /**
  1017. * Check if the there is a next step
  1018. * @return {boolean} true if the current date has not passed the end date
  1019. */
  1020. TimeStep.prototype.hasNext = function () {
  1021. return (this.current.valueOf() <= this._end.valueOf());
  1022. };
  1023. /**
  1024. * Do the next step
  1025. */
  1026. TimeStep.prototype.next = function() {
  1027. var prev = this.current.valueOf();
  1028. // Two cases, needed to prevent issues with switching daylight savings
  1029. // (end of March and end of October)
  1030. if (this.current.getMonth() < 6) {
  1031. switch (this.scale) {
  1032. case TimeStep.SCALE.MILLISECOND:
  1033. this.current = new Date(this.current.valueOf() + this.step); break;
  1034. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  1035. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  1036. case TimeStep.SCALE.HOUR:
  1037. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  1038. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  1039. var h = this.current.getHours();
  1040. this.current.setHours(h - (h % this.step));
  1041. break;
  1042. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1043. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  1044. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  1045. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  1046. default: break;
  1047. }
  1048. }
  1049. else {
  1050. switch (this.scale) {
  1051. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  1052. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  1053. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  1054. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  1055. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1056. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  1057. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  1058. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  1059. default: break;
  1060. }
  1061. }
  1062. if (this.step != 1) {
  1063. // round down to the correct major value
  1064. switch (this.scale) {
  1065. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  1066. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  1067. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  1068. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  1069. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1070. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  1071. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  1072. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  1073. default: break;
  1074. }
  1075. }
  1076. // safety mechanism: if current time is still unchanged, move to the end
  1077. if (this.current.valueOf() == prev) {
  1078. this.current = new Date(this._end.valueOf());
  1079. }
  1080. };
  1081. /**
  1082. * Get the current datetime
  1083. * @return {Date} current The current date
  1084. */
  1085. TimeStep.prototype.getCurrent = function() {
  1086. return this.current;
  1087. };
  1088. /**
  1089. * Set a custom scale. Autoscaling will be disabled.
  1090. * For example setScale(SCALE.MINUTES, 5) will result
  1091. * in minor steps of 5 minutes, and major steps of an hour.
  1092. *
  1093. * @param {TimeStep.SCALE} newScale
  1094. * A scale. Choose from SCALE.MILLISECOND,
  1095. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  1096. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  1097. * SCALE.YEAR.
  1098. * @param {Number} newStep A step size, by default 1. Choose for
  1099. * example 1, 2, 5, or 10.
  1100. */
  1101. TimeStep.prototype.setScale = function(newScale, newStep) {
  1102. this.scale = newScale;
  1103. if (newStep > 0) {
  1104. this.step = newStep;
  1105. }
  1106. this.autoScale = false;
  1107. };
  1108. /**
  1109. * Enable or disable autoscaling
  1110. * @param {boolean} enable If true, autoascaling is set true
  1111. */
  1112. TimeStep.prototype.setAutoScale = function (enable) {
  1113. this.autoScale = enable;
  1114. };
  1115. /**
  1116. * Automatically determine the scale that bests fits the provided minimum step
  1117. * @param {Number} [minimumStep] The minimum step size in milliseconds
  1118. */
  1119. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  1120. if (minimumStep == undefined) {
  1121. return;
  1122. }
  1123. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  1124. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  1125. var stepDay = (1000 * 60 * 60 * 24);
  1126. var stepHour = (1000 * 60 * 60);
  1127. var stepMinute = (1000 * 60);
  1128. var stepSecond = (1000);
  1129. var stepMillisecond= (1);
  1130. // find the smallest step that is larger than the provided minimumStep
  1131. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  1132. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  1133. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  1134. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  1135. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  1136. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  1137. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  1138. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  1139. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  1140. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  1141. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  1142. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  1143. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  1144. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  1145. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  1146. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  1147. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  1148. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  1149. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  1150. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  1151. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  1152. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  1153. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  1154. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  1155. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  1156. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  1157. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  1158. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  1159. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  1160. };
  1161. /**
  1162. * Snap a date to a rounded value. The snap intervals are dependent on the
  1163. * current scale and step.
  1164. * @param {Date} date the date to be snapped
  1165. */
  1166. TimeStep.prototype.snap = function(date) {
  1167. if (this.scale == TimeStep.SCALE.YEAR) {
  1168. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  1169. date.setFullYear(Math.round(year / this.step) * this.step);
  1170. date.setMonth(0);
  1171. date.setDate(0);
  1172. date.setHours(0);
  1173. date.setMinutes(0);
  1174. date.setSeconds(0);
  1175. date.setMilliseconds(0);
  1176. }
  1177. else if (this.scale == TimeStep.SCALE.MONTH) {
  1178. if (date.getDate() > 15) {
  1179. date.setDate(1);
  1180. date.setMonth(date.getMonth() + 1);
  1181. // important: first set Date to 1, after that change the month.
  1182. }
  1183. else {
  1184. date.setDate(1);
  1185. }
  1186. date.setHours(0);
  1187. date.setMinutes(0);
  1188. date.setSeconds(0);
  1189. date.setMilliseconds(0);
  1190. }
  1191. else if (this.scale == TimeStep.SCALE.DAY ||
  1192. this.scale == TimeStep.SCALE.WEEKDAY) {
  1193. //noinspection FallthroughInSwitchStatementJS
  1194. switch (this.step) {
  1195. case 5:
  1196. case 2:
  1197. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  1198. default:
  1199. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  1200. }
  1201. date.setMinutes(0);
  1202. date.setSeconds(0);
  1203. date.setMilliseconds(0);
  1204. }
  1205. else if (this.scale == TimeStep.SCALE.HOUR) {
  1206. switch (this.step) {
  1207. case 4:
  1208. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  1209. default:
  1210. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  1211. }
  1212. date.setSeconds(0);
  1213. date.setMilliseconds(0);
  1214. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  1215. //noinspection FallthroughInSwitchStatementJS
  1216. switch (this.step) {
  1217. case 15:
  1218. case 10:
  1219. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  1220. date.setSeconds(0);
  1221. break;
  1222. case 5:
  1223. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  1224. default:
  1225. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  1226. }
  1227. date.setMilliseconds(0);
  1228. }
  1229. else if (this.scale == TimeStep.SCALE.SECOND) {
  1230. //noinspection FallthroughInSwitchStatementJS
  1231. switch (this.step) {
  1232. case 15:
  1233. case 10:
  1234. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  1235. date.setMilliseconds(0);
  1236. break;
  1237. case 5:
  1238. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  1239. default:
  1240. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  1241. }
  1242. }
  1243. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  1244. var step = this.step > 5 ? this.step / 2 : 1;
  1245. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  1246. }
  1247. };
  1248. /**
  1249. * Check if the current value is a major value (for example when the step
  1250. * is DAY, a major value is each first day of the MONTH)
  1251. * @return {boolean} true if current date is major, else false.
  1252. */
  1253. TimeStep.prototype.isMajor = function() {
  1254. switch (this.scale) {
  1255. case TimeStep.SCALE.MILLISECOND:
  1256. return (this.current.getMilliseconds() == 0);
  1257. case TimeStep.SCALE.SECOND:
  1258. return (this.current.getSeconds() == 0);
  1259. case TimeStep.SCALE.MINUTE:
  1260. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  1261. // Note: this is no bug. Major label is equal for both minute and hour scale
  1262. case TimeStep.SCALE.HOUR:
  1263. return (this.current.getHours() == 0);
  1264. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1265. case TimeStep.SCALE.DAY:
  1266. return (this.current.getDate() == 1);
  1267. case TimeStep.SCALE.MONTH:
  1268. return (this.current.getMonth() == 0);
  1269. case TimeStep.SCALE.YEAR:
  1270. return false;
  1271. default:
  1272. return false;
  1273. }
  1274. };
  1275. /**
  1276. * Returns formatted text for the minor axislabel, depending on the current
  1277. * date and the scale. For example when scale is MINUTE, the current time is
  1278. * formatted as "hh:mm".
  1279. * @param {Date} [date] custom date. if not provided, current date is taken
  1280. */
  1281. TimeStep.prototype.getLabelMinor = function(date) {
  1282. if (date == undefined) {
  1283. date = this.current;
  1284. }
  1285. switch (this.scale) {
  1286. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  1287. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  1288. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  1289. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  1290. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  1291. case TimeStep.SCALE.DAY: return moment(date).format('D');
  1292. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  1293. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  1294. default: return '';
  1295. }
  1296. };
  1297. /**
  1298. * Returns formatted text for the major axis label, depending on the current
  1299. * date and the scale. For example when scale is MINUTE, the major scale is
  1300. * hours, and the hour will be formatted as "hh".
  1301. * @param {Date} [date] custom date. if not provided, current date is taken
  1302. */
  1303. TimeStep.prototype.getLabelMajor = function(date) {
  1304. if (date == undefined) {
  1305. date = this.current;
  1306. }
  1307. //noinspection FallthroughInSwitchStatementJS
  1308. switch (this.scale) {
  1309. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  1310. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  1311. case TimeStep.SCALE.MINUTE:
  1312. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  1313. case TimeStep.SCALE.WEEKDAY:
  1314. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  1315. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  1316. case TimeStep.SCALE.YEAR: return '';
  1317. default: return '';
  1318. }
  1319. };
  1320. /**
  1321. * DataSet
  1322. *
  1323. * Usage:
  1324. * var dataSet = new DataSet({
  1325. * fieldId: '_id',
  1326. * fieldTypes: {
  1327. * // ...
  1328. * }
  1329. * });
  1330. *
  1331. * dataSet.add(item);
  1332. * dataSet.add(data);
  1333. * dataSet.update(item);
  1334. * dataSet.update(data);
  1335. * dataSet.remove(id);
  1336. * dataSet.remove(ids);
  1337. * var data = dataSet.get();
  1338. * var data = dataSet.get(id);
  1339. * var data = dataSet.get(ids);
  1340. * var data = dataSet.get(ids, options, data);
  1341. * dataSet.clear();
  1342. *
  1343. * A data set can:
  1344. * - add/remove/update data
  1345. * - gives triggers upon changes in the data
  1346. * - can import/export data in various data formats
  1347. * @param {Object} [options] Available options:
  1348. * {String} fieldId Field name of the id in the
  1349. * items, 'id' by default.
  1350. * {Object.<String, String} fieldTypes
  1351. * A map with field names as key,
  1352. * and the field type as value.
  1353. */
  1354. function DataSet (options) {
  1355. var me = this;
  1356. this.options = options || {};
  1357. this.data = {}; // map with data indexed by id
  1358. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1359. this.fieldTypes = {}; // field types by field name
  1360. if (this.options.fieldTypes) {
  1361. util.forEach(this.options.fieldTypes, function (value, field) {
  1362. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1363. me.fieldTypes[field] = 'Date';
  1364. }
  1365. else {
  1366. me.fieldTypes[field] = value;
  1367. }
  1368. });
  1369. }
  1370. // event subscribers
  1371. this.subscribers = {};
  1372. this.internalIds = {}; // internally generated id's
  1373. }
  1374. /**
  1375. * Subscribe to an event, add an event listener
  1376. * @param {String} event Event name. Available events: 'put', 'update',
  1377. * 'remove'
  1378. * @param {function} callback Callback method. Called with three parameters:
  1379. * {String} event
  1380. * {Object | null} params
  1381. * {String} senderId
  1382. * @param {String} [id] Optional id for the sender, used to filter
  1383. * events triggered by the sender itself.
  1384. */
  1385. DataSet.prototype.subscribe = function (event, callback, id) {
  1386. var subscribers = this.subscribers[event];
  1387. if (!subscribers) {
  1388. subscribers = [];
  1389. this.subscribers[event] = subscribers;
  1390. }
  1391. subscribers.push({
  1392. id: id ? String(id) : null,
  1393. callback: callback
  1394. });
  1395. };
  1396. /**
  1397. * Unsubscribe from an event, remove an event listener
  1398. * @param {String} event
  1399. * @param {function} callback
  1400. */
  1401. DataSet.prototype.unsubscribe = function (event, callback) {
  1402. var subscribers = this.subscribers[event];
  1403. if (subscribers) {
  1404. this.subscribers[event] = subscribers.filter(function (listener) {
  1405. return (listener.callback != callback);
  1406. });
  1407. }
  1408. };
  1409. /**
  1410. * Trigger an event
  1411. * @param {String} event
  1412. * @param {Object | null} params
  1413. * @param {String} [senderId] Optional id of the sender. The event will
  1414. * be triggered for all subscribers except the
  1415. * sender itself.
  1416. * @private
  1417. */
  1418. DataSet.prototype._trigger = function (event, params, senderId) {
  1419. if (event == '*') {
  1420. throw new Error('Cannot trigger event *');
  1421. }
  1422. var subscribers = [];
  1423. if (event in this.subscribers) {
  1424. subscribers = subscribers.concat(this.subscribers[event]);
  1425. }
  1426. if ('*' in this.subscribers) {
  1427. subscribers = subscribers.concat(this.subscribers['*']);
  1428. }
  1429. subscribers.forEach(function (listener) {
  1430. if (listener.id != senderId && listener.callback) {
  1431. listener.callback(event, params, senderId || null);
  1432. }
  1433. });
  1434. };
  1435. /**
  1436. * Add data. Existing items with the same id will be overwritten.
  1437. * @param {Object | Array | DataTable} data
  1438. * @param {String} [senderId] Optional sender id, used to trigger events for
  1439. * all but this sender's event subscribers.
  1440. */
  1441. DataSet.prototype.add = function (data, senderId) {
  1442. var items = [],
  1443. id,
  1444. me = this;
  1445. if (data instanceof Array) {
  1446. // Array
  1447. data.forEach(function (item) {
  1448. var id = me._addItem(item);
  1449. items.push(id);
  1450. });
  1451. }
  1452. else if (util.isDataTable(data)) {
  1453. // Google DataTable
  1454. var columns = this._getColumnNames(data);
  1455. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1456. var item = {};
  1457. columns.forEach(function (field, col) {
  1458. item[field] = data.getValue(row, col);
  1459. });
  1460. id = me._addItem(item);
  1461. items.push(id);
  1462. }
  1463. }
  1464. else if (data instanceof Object) {
  1465. // Single item
  1466. id = me._addItem(data);
  1467. items.push(id);
  1468. }
  1469. else {
  1470. throw new Error('Unknown dataType');
  1471. }
  1472. this._trigger('add', {items: items}, senderId);
  1473. };
  1474. /**
  1475. * Update existing items. Items with the same id will be merged
  1476. * @param {Object | Array | DataTable} data
  1477. * @param {String} [senderId] Optional sender id, used to trigger events for
  1478. * all but this sender's event subscribers.
  1479. */
  1480. DataSet.prototype.update = function (data, senderId) {
  1481. var items = [],
  1482. id,
  1483. me = this;
  1484. if (data instanceof Array) {
  1485. // Array
  1486. data.forEach(function (item) {
  1487. var id = me._updateItem(item);
  1488. items.push(id);
  1489. });
  1490. }
  1491. else if (util.isDataTable(data)) {
  1492. // Google DataTable
  1493. var columns = this._getColumnNames(data);
  1494. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1495. var item = {};
  1496. columns.forEach(function (field, col) {
  1497. item[field] = data.getValue(row, col);
  1498. });
  1499. id = me._updateItem(item);
  1500. items.push(id);
  1501. }
  1502. }
  1503. else if (data instanceof Object) {
  1504. // Single item
  1505. id = me._updateItem(data);
  1506. items.push(id);
  1507. }
  1508. else {
  1509. throw new Error('Unknown dataType');
  1510. }
  1511. this._trigger('update', {items: items}, senderId);
  1512. };
  1513. /**
  1514. * Get a data item or multiple items
  1515. * @param {String | Number | Array | Object} [ids] Id of a single item, or an
  1516. * array with multiple id's, or
  1517. * undefined or an Object with options
  1518. * to retrieve all data.
  1519. * @param {Object} [options] Available options:
  1520. * {String} [type]
  1521. * 'DataTable' or 'Array' (default)
  1522. * {Object.<String, String>} [fieldTypes]
  1523. * {String[]} [fields] filter fields
  1524. * {function} [filter] filter items
  1525. * @param {Array | DataTable} [data] If provided, items will be appended
  1526. * to this array or table. Required
  1527. * in case of Google DataTable
  1528. * @return {Array | Object | DataTable | null} data
  1529. * @throws Error
  1530. */
  1531. DataSet.prototype.get = function (ids, options, data) {
  1532. var me = this;
  1533. // TODO: simplify handling the inputs. It's quite a mess right now...
  1534. // shift arguments when first argument contains the options
  1535. if (util.getType(ids) == 'Object') {
  1536. data = options;
  1537. options = ids;
  1538. ids = undefined;
  1539. }
  1540. var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
  1541. var fields = options && options.fields;
  1542. var filter = options && options.filter;
  1543. // determine the return type
  1544. var type;
  1545. if (options && options.type) {
  1546. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1547. if (data && (type != util.getType(data))) {
  1548. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1549. 'does not correspond with specified options.type (' + options.type + ')');
  1550. }
  1551. if (type == 'DataTable' && !util.isDataTable(data)) {
  1552. throw new Error('Parameter "data" must be a DataTable ' +
  1553. 'when options.type is "DataTable"');
  1554. }
  1555. }
  1556. else if (data) {
  1557. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1558. }
  1559. else {
  1560. type = 'Array';
  1561. }
  1562. if (type == 'DataTable') {
  1563. // return a Google DataTable
  1564. var columns = this._getColumnNames(data);
  1565. if (ids == undefined) {
  1566. // return all data
  1567. util.forEach(this.data, function (item) {
  1568. var castedItem = me._castItem(item);
  1569. if (!castedItem || filter(castedItem)) {
  1570. me._appendRow(data, columns, castedItem);
  1571. }
  1572. });
  1573. }
  1574. else if (util.isNumber(ids) || util.isString(ids)) {
  1575. var item = me._castItem(me.data[ids], fieldTypes, fields);
  1576. this._appendRow(data, columns, item);
  1577. }
  1578. else if (ids instanceof Array) {
  1579. ids.forEach(function (id) {
  1580. var castedItem = me._castItem(me.data[id], fieldTypes, fields);
  1581. if (!castedItem || filter(castedItem)) {
  1582. me._appendRow(data, columns, castedItem);
  1583. }
  1584. });
  1585. }
  1586. else {
  1587. throw new TypeError('Parameter "ids" must be ' +
  1588. 'undefined, a String, Number, or Array');
  1589. }
  1590. }
  1591. else {
  1592. // return an array
  1593. data = data || [];
  1594. if (ids == undefined) {
  1595. // return all data
  1596. util.forEach(this.data, function (item) {
  1597. var castedItem = me._castItem(item, fieldTypes, fields);
  1598. if (!filter || filter(castedItem)) {
  1599. data.push(castedItem);
  1600. }
  1601. });
  1602. }
  1603. else if (util.isNumber(ids) || util.isString(ids)) {
  1604. // return a single item
  1605. return this._castItem(me.data[ids], fieldTypes, fields);
  1606. }
  1607. else if (ids instanceof Array) {
  1608. ids.forEach(function (id) {
  1609. var castedItem = me._castItem(me.data[id], fieldTypes, fields);
  1610. if (!filter || filter(castedItem)) {
  1611. data.push(castedItem);
  1612. }
  1613. });
  1614. }
  1615. else {
  1616. throw new TypeError('Parameter "ids" must be ' +
  1617. 'undefined, a String, Number, or Array');
  1618. }
  1619. }
  1620. return data;
  1621. };
  1622. /**
  1623. * Execute a callback function for every item in the dataset.
  1624. * The order of the items is not determined.
  1625. * @param {function} callback
  1626. * @param {Object} [options] Available options:
  1627. * {Object.<String, String>} [fieldTypes]
  1628. * {String[]} [fields] filter fields
  1629. * {function} [filter] filter items
  1630. */
  1631. DataSet.prototype.forEach = function (callback, options) {
  1632. var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
  1633. var fields = options && options.fields;
  1634. var filter = options && options.filter;
  1635. var me = this;
  1636. util.forEach(this.data, function (item, id) {
  1637. var castedItem = me._castItem(item, fieldTypes, fields);
  1638. if (!filter || filter(castedItem)) {
  1639. callback(castedItem, id);
  1640. }
  1641. });
  1642. };
  1643. /**
  1644. * Map every item in the dataset.
  1645. * @param {function} callback
  1646. * @param {Object} [options] Available options:
  1647. * {Object.<String, String>} [fieldTypes]
  1648. * {String[]} [fields] filter fields
  1649. * {function} [filter] filter items
  1650. * @return {Object[]} mappedItems
  1651. */
  1652. DataSet.prototype.map = function (callback, options) {
  1653. var fieldTypes = this._mergeFieldTypes(options && options.fieldTypes);
  1654. var fields = options && options.fields;
  1655. var filter = options && options.filter;
  1656. var me = this;
  1657. var mappedItems = [];
  1658. util.forEach(this.data, function (item, id) {
  1659. var castedItem = me._castItem(item, fieldTypes, fields);
  1660. if (!filter || filter(castedItem)) {
  1661. var mappedItem = callback(castedItem, id);
  1662. mappedItems.push(mappedItem);
  1663. }
  1664. });
  1665. return mappedItems;
  1666. };
  1667. /**
  1668. * Merge the provided field types with the datasets fieldtypes
  1669. * @param {Object} fieldTypes
  1670. * @returns {Object} mergedFieldTypes
  1671. * @private
  1672. */
  1673. DataSet.prototype._mergeFieldTypes = function (fieldTypes) {
  1674. var merged = {};
  1675. // extend with the datasets fieldTypes
  1676. if (this.options && this.options.fieldTypes) {
  1677. util.forEach(this.options.fieldTypes, function (value, field) {
  1678. merged[field] = value;
  1679. });
  1680. }
  1681. // extend with provided fieldTypes
  1682. if (fieldTypes) {
  1683. util.forEach(fieldTypes, function (value, field) {
  1684. merged[field] = value;
  1685. });
  1686. }
  1687. return merged;
  1688. };
  1689. /**
  1690. * Remove an object by pointer or by id
  1691. * @param {String | Number | Object | Array} id Object or id, or an array with
  1692. * objects or ids to be removed
  1693. * @param {String} [senderId] Optional sender id, used to trigger events for
  1694. * all but this sender's event subscribers.
  1695. */
  1696. DataSet.prototype.remove = function (id, senderId) {
  1697. var items = [],
  1698. me = this;
  1699. if (util.isNumber(id) || util.isString(id)) {
  1700. delete this.data[id];
  1701. delete this.internalIds[id];
  1702. items.push(id);
  1703. }
  1704. else if (id instanceof Array) {
  1705. id.forEach(function (id) {
  1706. me.remove(id);
  1707. });
  1708. items = items.concat(id);
  1709. }
  1710. else if (id instanceof Object) {
  1711. // search for the object
  1712. for (var i in this.data) {
  1713. if (this.data.hasOwnProperty(i)) {
  1714. if (this.data[i] == id) {
  1715. delete this.data[i];
  1716. delete this.internalIds[i];
  1717. items.push(i);
  1718. }
  1719. }
  1720. }
  1721. }
  1722. this._trigger('remove', {items: items}, senderId);
  1723. };
  1724. /**
  1725. * Clear the data
  1726. * @param {String} [senderId] Optional sender id, used to trigger events for
  1727. * all but this sender's event subscribers.
  1728. */
  1729. DataSet.prototype.clear = function (senderId) {
  1730. var ids = Object.keys(this.data);
  1731. this.data = {};
  1732. this.internalIds = {};
  1733. this._trigger('remove', {items: ids}, senderId);
  1734. };
  1735. /**
  1736. * Find the item with maximum value of a specified field
  1737. * @param {String} field
  1738. * @return {Object | null} item Item containing max value, or null if no items
  1739. */
  1740. DataSet.prototype.max = function (field) {
  1741. var data = this.data,
  1742. ids = Object.keys(data);
  1743. var max = null;
  1744. var maxField = null;
  1745. ids.forEach(function (id) {
  1746. var item = data[id];
  1747. var itemField = item[field];
  1748. if (itemField != null && (!max || itemField > maxField)) {
  1749. max = item;
  1750. maxField = itemField;
  1751. }
  1752. });
  1753. return max;
  1754. };
  1755. /**
  1756. * Find the item with minimum value of a specified field
  1757. * @param {String} field
  1758. * @return {Object | null} item Item containing max value, or null if no items
  1759. */
  1760. DataSet.prototype.min = function (field) {
  1761. var data = this.data,
  1762. ids = Object.keys(data);
  1763. var min = null;
  1764. var minField = null;
  1765. ids.forEach(function (id) {
  1766. var item = data[id];
  1767. var itemField = item[field];
  1768. if (itemField != null && (!min || itemField < minField)) {
  1769. min = item;
  1770. minField = itemField;
  1771. }
  1772. });
  1773. return min;
  1774. };
  1775. /**
  1776. * Find all distinct values of a specified field
  1777. * @param {String} field
  1778. * @return {Array} values Array containing all distinct values. If the data
  1779. * items do not contain the specified field, an array
  1780. * containing a single value undefined is returned.
  1781. * The returned array is unordered.
  1782. */
  1783. DataSet.prototype.distinct = function (field) {
  1784. var data = this.data,
  1785. values = [],
  1786. fieldType = this.options.fieldTypes[field],
  1787. count = 0;
  1788. for (var prop in data) {
  1789. if (data.hasOwnProperty(prop)) {
  1790. var item = data[prop];
  1791. var value = util.cast(item[field], fieldType);
  1792. var exists = false;
  1793. for (var i = 0; i < count; i++) {
  1794. if (values[i] == value) {
  1795. exists = true;
  1796. break;
  1797. }
  1798. }
  1799. if (!exists) {
  1800. values[count] = value;
  1801. count++;
  1802. }
  1803. }
  1804. }
  1805. return values;
  1806. };
  1807. /**
  1808. * Add a single item
  1809. * @param {Object} item
  1810. * @return {String} id
  1811. * @private
  1812. */
  1813. DataSet.prototype._addItem = function (item) {
  1814. var id = item[this.fieldId];
  1815. if (id == undefined) {
  1816. // generate an id
  1817. id = util.randomUUID();
  1818. item[this.fieldId] = id;
  1819. this.internalIds[id] = item;
  1820. }
  1821. var d = {};
  1822. for (var field in item) {
  1823. if (item.hasOwnProperty(field)) {
  1824. var type = this.fieldTypes[field]; // type may be undefined
  1825. d[field] = util.cast(item[field], type);
  1826. }
  1827. }
  1828. this.data[id] = d;
  1829. //TODO: fail when an item with this id already exists?
  1830. return id;
  1831. };
  1832. /**
  1833. * Cast and filter the fields of an item
  1834. * @param {Object | undefined} item
  1835. * @param {Object.<String, String>} [fieldTypes]
  1836. * @param {String[]} [fields]
  1837. * @return {Object | null} castedItem
  1838. * @private
  1839. */
  1840. DataSet.prototype._castItem = function (item, fieldTypes, fields) {
  1841. var clone,
  1842. fieldId = this.fieldId,
  1843. internalIds = this.internalIds;
  1844. if (item) {
  1845. clone = {};
  1846. fieldTypes = fieldTypes || {};
  1847. if (fields) {
  1848. // output filtered fields
  1849. util.forEach(item, function (value, field) {
  1850. if (fields.indexOf(field) != -1) {
  1851. clone[field] = util.cast(value, fieldTypes[field]);
  1852. }
  1853. });
  1854. }
  1855. else {
  1856. // output all fields, except internal ids
  1857. util.forEach(item, function (value, field) {
  1858. if (field != fieldId || !(value in internalIds)) {
  1859. clone[field] = util.cast(value, fieldTypes[field]);
  1860. }
  1861. });
  1862. }
  1863. }
  1864. else {
  1865. clone = null;
  1866. }
  1867. return clone;
  1868. };
  1869. /**
  1870. * Update a single item: merge with existing item
  1871. * @param {Object} item
  1872. * @return {String} id
  1873. * @private
  1874. */
  1875. DataSet.prototype._updateItem = function (item) {
  1876. var id = item[this.fieldId];
  1877. if (id == undefined) {
  1878. throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
  1879. }
  1880. var d = this.data[id];
  1881. if (d) {
  1882. // merge with current item
  1883. for (var field in item) {
  1884. if (item.hasOwnProperty(field)) {
  1885. var type = this.fieldTypes[field]; // type may be undefined
  1886. d[field] = util.cast(item[field], type);
  1887. }
  1888. }
  1889. }
  1890. else {
  1891. // create new item
  1892. this._addItem(item);
  1893. }
  1894. return id;
  1895. };
  1896. /**
  1897. * Get an array with the column names of a Google DataTable
  1898. * @param {DataTable} dataTable
  1899. * @return {Array} columnNames
  1900. * @private
  1901. */
  1902. DataSet.prototype._getColumnNames = function (dataTable) {
  1903. var columns = [];
  1904. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1905. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1906. }
  1907. return columns;
  1908. };
  1909. /**
  1910. * Append an item as a row to the dataTable
  1911. * @param dataTable
  1912. * @param columns
  1913. * @param item
  1914. * @private
  1915. */
  1916. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1917. var row = dataTable.addRow();
  1918. columns.forEach(function (field, col) {
  1919. dataTable.setValue(row, col, item[field]);
  1920. });
  1921. };
  1922. /**
  1923. * @constructor Stack
  1924. * Stacks items on top of each other.
  1925. * @param {ItemSet} parent
  1926. * @param {Object} [options]
  1927. */
  1928. function Stack (parent, options) {
  1929. this.parent = parent;
  1930. this.options = {
  1931. order: function (a, b) {
  1932. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  1933. // Order: ranges over non-ranges, ranged ordered by width, and
  1934. // lastly ordered by start.
  1935. if (a instanceof ItemRange) {
  1936. if (b instanceof ItemRange) {
  1937. var aInt = (a.data.end - a.data.start);
  1938. var bInt = (b.data.end - b.data.start);
  1939. return (aInt - bInt) || (a.data.start - b.data.start);
  1940. }
  1941. else {
  1942. return -1;
  1943. }
  1944. }
  1945. else {
  1946. if (b instanceof ItemRange) {
  1947. return 1;
  1948. }
  1949. else {
  1950. return (a.data.start - b.data.start);
  1951. }
  1952. }
  1953. }
  1954. };
  1955. this.ordered = []; // ordered items
  1956. this.setOptions(options);
  1957. }
  1958. /**
  1959. * Set options for the stack
  1960. * @param {Object} options Available options:
  1961. * {ItemSet} parent
  1962. * {Number} margin
  1963. * {function} order Stacking order
  1964. */
  1965. Stack.prototype.setOptions = function setOptions (options) {
  1966. util.extend(this.options, options);
  1967. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  1968. };
  1969. /**
  1970. * Stack the items such that they don't overlap. The items will have a minimal
  1971. * distance equal to options.margin.item.
  1972. */
  1973. Stack.prototype.update = function update() {
  1974. this._order();
  1975. this._stack();
  1976. };
  1977. /**
  1978. * Order the items. The items are ordered by width first, and by left position
  1979. * second.
  1980. * If a custom order function has been provided via the options, then this will
  1981. * be used.
  1982. * @private
  1983. */
  1984. Stack.prototype._order = function _order () {
  1985. var items = this.parent.items;
  1986. if (!items) {
  1987. throw new Error('Cannot stack items: parent does not contain items');
  1988. }
  1989. // TODO: store the sorted items, to have less work later on
  1990. var ordered = [];
  1991. var index = 0;
  1992. // items is a map (no array)
  1993. util.forEach(items, function (item) {
  1994. if (item.visible) {
  1995. ordered[index] = item;
  1996. index++;
  1997. }
  1998. });
  1999. //if a customer stack order function exists, use it.
  2000. var order = this.options.order;
  2001. if (!(typeof this.options.order === 'function')) {
  2002. throw new Error('Option order must be a function');
  2003. }
  2004. ordered.sort(order);
  2005. this.ordered = ordered;
  2006. };
  2007. /**
  2008. * Adjust vertical positions of the events such that they don't overlap each
  2009. * other.
  2010. * @private
  2011. */
  2012. Stack.prototype._stack = function _stack () {
  2013. var i,
  2014. iMax,
  2015. ordered = this.ordered,
  2016. options = this.options,
  2017. axisOnTop = (options.orientation == 'top'),
  2018. margin = options.margin && options.margin.item || 0;
  2019. // calculate new, non-overlapping positions
  2020. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2021. var item = ordered[i];
  2022. var collidingItem = null;
  2023. do {
  2024. // TODO: optimize checking for overlap. when there is a gap without items,
  2025. // you only need to check for items from the next item on, not from zero
  2026. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2027. if (collidingItem != null) {
  2028. // There is a collision. Reposition the event above the colliding element
  2029. if (axisOnTop) {
  2030. item.top = collidingItem.top + collidingItem.height + margin;
  2031. }
  2032. else {
  2033. item.top = collidingItem.top - item.height - margin;
  2034. }
  2035. }
  2036. } while (collidingItem);
  2037. }
  2038. };
  2039. /**
  2040. * Check if the destiny position of given item overlaps with any
  2041. * of the other items from index itemStart to itemEnd.
  2042. * @param {Array} items Array with items
  2043. * @param {int} itemIndex Number of the item to be checked for overlap
  2044. * @param {int} itemStart First item to be checked.
  2045. * @param {int} itemEnd Last item to be checked.
  2046. * @return {Object | null} colliding item, or undefined when no collisions
  2047. * @param {Number} margin A minimum required margin.
  2048. * If margin is provided, the two items will be
  2049. * marked colliding when they overlap or
  2050. * when the margin between the two is smaller than
  2051. * the requested margin.
  2052. */
  2053. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2054. itemStart, itemEnd, margin) {
  2055. var collision = this.collision;
  2056. // we loop from end to start, as we suppose that the chance of a
  2057. // collision is larger for items at the end, so check these first.
  2058. var a = items[itemIndex];
  2059. for (var i = itemEnd; i >= itemStart; i--) {
  2060. var b = items[i];
  2061. if (collision(a, b, margin)) {
  2062. if (i != itemIndex) {
  2063. return b;
  2064. }
  2065. }
  2066. }
  2067. return null;
  2068. };
  2069. /**
  2070. * Test if the two provided items collide
  2071. * The items must have parameters left, width, top, and height.
  2072. * @param {Component} a The first item
  2073. * @param {Component} b The second item
  2074. * @param {Number} margin A minimum required margin.
  2075. * If margin is provided, the two items will be
  2076. * marked colliding when they overlap or
  2077. * when the margin between the two is smaller than
  2078. * the requested margin.
  2079. * @return {boolean} true if a and b collide, else false
  2080. */
  2081. Stack.prototype.collision = function collision (a, b, margin) {
  2082. return ((a.left - margin) < (b.left + b.width) &&
  2083. (a.left + a.width + margin) > b.left &&
  2084. (a.top - margin) < (b.top + b.height) &&
  2085. (a.top + a.height + margin) > b.top);
  2086. };
  2087. /**
  2088. * @constructor Range
  2089. * A Range controls a numeric range with a start and end value.
  2090. * The Range adjusts the range based on mouse events or programmatic changes,
  2091. * and triggers events when the range is changing or has been changed.
  2092. * @param {Object} [options] See description at Range.setOptions
  2093. * @extends Controller
  2094. */
  2095. function Range(options) {
  2096. this.id = util.randomUUID();
  2097. this.start = 0; // Number
  2098. this.end = 0; // Number
  2099. this.options = {
  2100. min: null,
  2101. max: null,
  2102. zoomMin: null,
  2103. zoomMax: null
  2104. };
  2105. this.setOptions(options);
  2106. this.listeners = [];
  2107. }
  2108. /**
  2109. * Set options for the range controller
  2110. * @param {Object} options Available options:
  2111. * {Number} start Set start value of the range
  2112. * {Number} end Set end value of the range
  2113. * {Number} min Minimum value for start
  2114. * {Number} max Maximum value for end
  2115. * {Number} zoomMin Set a minimum value for
  2116. * (end - start).
  2117. * {Number} zoomMax Set a maximum value for
  2118. * (end - start).
  2119. */
  2120. Range.prototype.setOptions = function (options) {
  2121. util.extend(this.options, options);
  2122. if (options.start != null || options.end != null) {
  2123. this.setRange(options.start, options.end);
  2124. }
  2125. };
  2126. /**
  2127. * Add listeners for mouse and touch events to the component
  2128. * @param {Component} component
  2129. * @param {String} event Available events: 'move', 'zoom'
  2130. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2131. */
  2132. Range.prototype.subscribe = function (component, event, direction) {
  2133. var me = this;
  2134. var listener;
  2135. if (direction != 'horizontal' && direction != 'vertical') {
  2136. throw new TypeError('Unknown direction "' + direction + '". ' +
  2137. 'Choose "horizontal" or "vertical".');
  2138. }
  2139. //noinspection FallthroughInSwitchStatementJS
  2140. if (event == 'move') {
  2141. listener = {
  2142. component: component,
  2143. event: event,
  2144. direction: direction,
  2145. callback: function (event) {
  2146. me._onMouseDown(event, listener);
  2147. },
  2148. params: {}
  2149. };
  2150. component.on('mousedown', listener.callback);
  2151. me.listeners.push(listener);
  2152. }
  2153. else if (event == 'zoom') {
  2154. listener = {
  2155. component: component,
  2156. event: event,
  2157. direction: direction,
  2158. callback: function (event) {
  2159. me._onMouseWheel(event, listener);
  2160. },
  2161. params: {}
  2162. };
  2163. component.on('mousewheel', listener.callback);
  2164. me.listeners.push(listener);
  2165. }
  2166. else {
  2167. throw new TypeError('Unknown event "' + event + '". ' +
  2168. 'Choose "move" or "zoom".');
  2169. }
  2170. };
  2171. /**
  2172. * Event handler
  2173. * @param {String} event name of the event, for example 'click', 'mousemove'
  2174. * @param {function} callback callback handler, invoked with the raw HTML Event
  2175. * as parameter.
  2176. */
  2177. Range.prototype.on = function (event, callback) {
  2178. events.addListener(this, event, callback);
  2179. };
  2180. /**
  2181. * Trigger an event
  2182. * @param {String} event name of the event, available events: 'rangechange',
  2183. * 'rangechanged'
  2184. * @private
  2185. */
  2186. Range.prototype._trigger = function (event) {
  2187. events.trigger(this, event, {
  2188. start: this.start,
  2189. end: this.end
  2190. });
  2191. };
  2192. /**
  2193. * Set a new start and end range
  2194. * @param {Number} start
  2195. * @param {Number} end
  2196. */
  2197. Range.prototype.setRange = function(start, end) {
  2198. var changed = this._applyRange(start, end);
  2199. if (changed) {
  2200. this._trigger('rangechange');
  2201. this._trigger('rangechanged');
  2202. }
  2203. };
  2204. /**
  2205. * Set a new start and end range. This method is the same as setRange, but
  2206. * does not trigger a range change and range changed event, and it returns
  2207. * true when the range is changed
  2208. * @param {Number} start
  2209. * @param {Number} end
  2210. * @return {Boolean} changed
  2211. * @private
  2212. */
  2213. Range.prototype._applyRange = function(start, end) {
  2214. var newStart = (start != null) ? util.cast(start, 'Number') : this.start;
  2215. var newEnd = (end != null) ? util.cast(end, 'Number') : this.end;
  2216. var diff;
  2217. // check for valid number
  2218. if (isNaN(newStart)) {
  2219. throw new Error('Invalid start "' + start + '"');
  2220. }
  2221. if (isNaN(newEnd)) {
  2222. throw new Error('Invalid end "' + end + '"');
  2223. }
  2224. // prevent start < end
  2225. if (newEnd < newStart) {
  2226. newEnd = newStart;
  2227. }
  2228. // prevent start < min
  2229. if (this.options.min != null) {
  2230. var min = this.options.min.valueOf();
  2231. if (newStart < min) {
  2232. diff = (min - newStart);
  2233. newStart += diff;
  2234. newEnd += diff;
  2235. }
  2236. }
  2237. // prevent end > max
  2238. if (this.options.max != null) {
  2239. var max = this.options.max.valueOf();
  2240. if (newEnd > max) {
  2241. diff = (newEnd - max);
  2242. newStart -= diff;
  2243. newEnd -= diff;
  2244. }
  2245. }
  2246. // prevent (end-start) > zoomMin
  2247. if (this.options.zoomMin != null) {
  2248. var zoomMin = this.options.zoomMin.valueOf();
  2249. if (zoomMin < 0) {
  2250. zoomMin = 0;
  2251. }
  2252. if ((newEnd - newStart) < zoomMin) {
  2253. if ((this.end - this.start) > zoomMin) {
  2254. // zoom to the minimum
  2255. diff = (zoomMin - (newEnd - newStart));
  2256. newStart -= diff / 2;
  2257. newEnd += diff / 2;
  2258. }
  2259. else {
  2260. // ingore this action, we are already zoomed to the minimum
  2261. newStart = this.start;
  2262. newEnd = this.end;
  2263. }
  2264. }
  2265. }
  2266. // prevent (end-start) > zoomMin
  2267. if (this.options.zoomMax != null) {
  2268. var zoomMax = this.options.zoomMax.valueOf();
  2269. if (zoomMax < 0) {
  2270. zoomMax = 0;
  2271. }
  2272. if ((newEnd - newStart) > zoomMax) {
  2273. if ((this.end - this.start) < zoomMax) {
  2274. // zoom to the maximum
  2275. diff = ((newEnd - newStart) - zoomMax);
  2276. newStart += diff / 2;
  2277. newEnd -= diff / 2;
  2278. }
  2279. else {
  2280. // ingore this action, we are already zoomed to the maximum
  2281. newStart = this.start;
  2282. newEnd = this.end;
  2283. }
  2284. }
  2285. }
  2286. var changed = (this.start != newStart || this.end != newEnd);
  2287. this.start = newStart;
  2288. this.end = newEnd;
  2289. return changed;
  2290. };
  2291. /**
  2292. * Retrieve the current range.
  2293. * @return {Object} An object with start and end properties
  2294. */
  2295. Range.prototype.getRange = function() {
  2296. return {
  2297. start: this.start,
  2298. end: this.end
  2299. };
  2300. };
  2301. /**
  2302. * Calculate the conversion offset and factor for current range, based on
  2303. * the provided width
  2304. * @param {Number} width
  2305. * @returns {{offset: number, factor: number}} conversion
  2306. */
  2307. Range.prototype.conversion = function (width) {
  2308. var start = this.start;
  2309. var end = this.end;
  2310. return Range.conversion(this.start, this.end, width);
  2311. };
  2312. /**
  2313. * Static method to calculate the conversion offset and factor for a range,
  2314. * based on the provided start, end, and width
  2315. * @param {Number} start
  2316. * @param {Number} end
  2317. * @param {Number} width
  2318. * @returns {{offset: number, factor: number}} conversion
  2319. */
  2320. Range.conversion = function (start, end, width) {
  2321. if (width != 0 && (end - start != 0)) {
  2322. return {
  2323. offset: start,
  2324. factor: width / (end - start)
  2325. }
  2326. }
  2327. else {
  2328. return {
  2329. offset: 0,
  2330. factor: 1
  2331. };
  2332. }
  2333. };
  2334. /**
  2335. * Start moving horizontally or vertically
  2336. * @param {Event} event
  2337. * @param {Object} listener Listener containing the component and params
  2338. * @private
  2339. */
  2340. Range.prototype._onMouseDown = function(event, listener) {
  2341. event = event || window.event;
  2342. var params = listener.params;
  2343. // only react on left mouse button down
  2344. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2345. if (!leftButtonDown) {
  2346. return;
  2347. }
  2348. // get mouse position
  2349. params.mouseX = util.getPageX(event);
  2350. params.mouseY = util.getPageY(event);
  2351. params.previousLeft = 0;
  2352. params.previousOffset = 0;
  2353. params.moved = false;
  2354. params.start = this.start;
  2355. params.end = this.end;
  2356. var frame = listener.component.frame;
  2357. if (frame) {
  2358. frame.style.cursor = 'move';
  2359. }
  2360. // add event listeners to handle moving the contents
  2361. // we store the function onmousemove and onmouseup in the timeaxis,
  2362. // so we can remove the eventlisteners lateron in the function onmouseup
  2363. var me = this;
  2364. if (!params.onMouseMove) {
  2365. params.onMouseMove = function (event) {
  2366. me._onMouseMove(event, listener);
  2367. };
  2368. util.addEventListener(document, "mousemove", params.onMouseMove);
  2369. }
  2370. if (!params.onMouseUp) {
  2371. params.onMouseUp = function (event) {
  2372. me._onMouseUp(event, listener);
  2373. };
  2374. util.addEventListener(document, "mouseup", params.onMouseUp);
  2375. }
  2376. util.preventDefault(event);
  2377. };
  2378. /**
  2379. * Perform moving operating.
  2380. * This function activated from within the funcion TimeAxis._onMouseDown().
  2381. * @param {Event} event
  2382. * @param {Object} listener
  2383. * @private
  2384. */
  2385. Range.prototype._onMouseMove = function (event, listener) {
  2386. event = event || window.event;
  2387. var params = listener.params;
  2388. // calculate change in mouse position
  2389. var mouseX = util.getPageX(event);
  2390. var mouseY = util.getPageY(event);
  2391. if (params.mouseX == undefined) {
  2392. params.mouseX = mouseX;
  2393. }
  2394. if (params.mouseY == undefined) {
  2395. params.mouseY = mouseY;
  2396. }
  2397. var diffX = mouseX - params.mouseX;
  2398. var diffY = mouseY - params.mouseY;
  2399. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  2400. // if mouse movement is big enough, register it as a "moved" event
  2401. if (Math.abs(diff) >= 1) {
  2402. params.moved = true;
  2403. }
  2404. var interval = (params.end - params.start);
  2405. var width = (listener.direction == 'horizontal') ?
  2406. listener.component.width : listener.component.height;
  2407. var diffRange = -diff / width * interval;
  2408. this._applyRange(params.start + diffRange, params.end + diffRange);
  2409. // fire a rangechange event
  2410. this._trigger('rangechange');
  2411. util.preventDefault(event);
  2412. };
  2413. /**
  2414. * Stop moving operating.
  2415. * This function activated from within the function Range._onMouseDown().
  2416. * @param {event} event
  2417. * @param {Object} listener
  2418. * @private
  2419. */
  2420. Range.prototype._onMouseUp = function (event, listener) {
  2421. event = event || window.event;
  2422. var params = listener.params;
  2423. if (listener.component.frame) {
  2424. listener.component.frame.style.cursor = 'auto';
  2425. }
  2426. // remove event listeners here, important for Safari
  2427. if (params.onMouseMove) {
  2428. util.removeEventListener(document, "mousemove", params.onMouseMove);
  2429. params.onMouseMove = null;
  2430. }
  2431. if (params.onMouseUp) {
  2432. util.removeEventListener(document, "mouseup", params.onMouseUp);
  2433. params.onMouseUp = null;
  2434. }
  2435. //util.preventDefault(event);
  2436. if (params.moved) {
  2437. // fire a rangechanged event
  2438. this._trigger('rangechanged');
  2439. }
  2440. };
  2441. /**
  2442. * Event handler for mouse wheel event, used to zoom
  2443. * Code from http://adomas.org/javascript-mouse-wheel/
  2444. * @param {Event} event
  2445. * @param {Object} listener
  2446. * @private
  2447. */
  2448. Range.prototype._onMouseWheel = function(event, listener) {
  2449. event = event || window.event;
  2450. // retrieve delta
  2451. var delta = 0;
  2452. if (event.wheelDelta) { /* IE/Opera. */
  2453. delta = event.wheelDelta / 120;
  2454. } else if (event.detail) { /* Mozilla case. */
  2455. // In Mozilla, sign of delta is different than in IE.
  2456. // Also, delta is multiple of 3.
  2457. delta = -event.detail / 3;
  2458. }
  2459. // If delta is nonzero, handle it.
  2460. // Basically, delta is now positive if wheel was scrolled up,
  2461. // and negative, if wheel was scrolled down.
  2462. if (delta) {
  2463. var me = this;
  2464. var zoom = function () {
  2465. // perform the zoom action. Delta is normally 1 or -1
  2466. var zoomFactor = delta / 5.0;
  2467. var zoomAround = null;
  2468. var frame = listener.component.frame;
  2469. if (frame) {
  2470. var size, conversion;
  2471. if (listener.direction == 'horizontal') {
  2472. size = listener.component.width;
  2473. conversion = me.conversion(size);
  2474. var frameLeft = util.getAbsoluteLeft(frame);
  2475. var mouseX = util.getPageX(event);
  2476. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  2477. }
  2478. else {
  2479. size = listener.component.height;
  2480. conversion = me.conversion(size);
  2481. var frameTop = util.getAbsoluteTop(frame);
  2482. var mouseY = util.getPageY(event);
  2483. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  2484. }
  2485. }
  2486. me.zoom(zoomFactor, zoomAround);
  2487. };
  2488. zoom();
  2489. }
  2490. // Prevent default actions caused by mouse wheel.
  2491. // That might be ugly, but we handle scrolls somehow
  2492. // anyway, so don't bother here...
  2493. util.preventDefault(event);
  2494. };
  2495. /**
  2496. * Zoom the range the given zoomfactor in or out. Start and end date will
  2497. * be adjusted, and the timeline will be redrawn. You can optionally give a
  2498. * date around which to zoom.
  2499. * For example, try zoomfactor = 0.1 or -0.1
  2500. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  2501. * negative value will zoom out
  2502. * @param {Number} zoomAround Value around which will be zoomed. Optional
  2503. */
  2504. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  2505. // if zoomAroundDate is not provided, take it half between start Date and end Date
  2506. if (zoomAround == null) {
  2507. zoomAround = (this.start + this.end) / 2;
  2508. }
  2509. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  2510. // result in a start>=end )
  2511. if (zoomFactor >= 1) {
  2512. zoomFactor = 0.9;
  2513. }
  2514. if (zoomFactor <= -1) {
  2515. zoomFactor = -0.9;
  2516. }
  2517. // adjust a negative factor such that zooming in with 0.1 equals zooming
  2518. // out with a factor -0.1
  2519. if (zoomFactor < 0) {
  2520. zoomFactor = zoomFactor / (1 + zoomFactor);
  2521. }
  2522. // zoom start and end relative to the zoomAround value
  2523. var startDiff = (this.start - zoomAround);
  2524. var endDiff = (this.end - zoomAround);
  2525. // calculate new start and end
  2526. var newStart = this.start - startDiff * zoomFactor;
  2527. var newEnd = this.end - endDiff * zoomFactor;
  2528. this.setRange(newStart, newEnd);
  2529. };
  2530. /**
  2531. * Move the range with a given factor to the left or right. Start and end
  2532. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  2533. * @param {Number} moveFactor Moving amount. Positive value will move right,
  2534. * negative value will move left
  2535. */
  2536. Range.prototype.move = function(moveFactor) {
  2537. // zoom start Date and end Date relative to the zoomAroundDate
  2538. var diff = (this.end - this.start);
  2539. // apply new values
  2540. var newStart = this.start + diff * moveFactor;
  2541. var newEnd = this.end + diff * moveFactor;
  2542. // TODO: reckon with min and max range
  2543. this.start = newStart;
  2544. this.end = newEnd;
  2545. };
  2546. /**
  2547. * An event bus can be used to emit events, and to subscribe to events
  2548. * @constructor EventBus
  2549. */
  2550. function EventBus() {
  2551. this.subscriptions = [];
  2552. }
  2553. /**
  2554. * Subscribe to an event
  2555. * @param {String | RegExp} event The event can be a regular expression, or
  2556. * a string with wildcards, like 'server.*'.
  2557. * @param {function} callback. Callback are called with three parameters:
  2558. * {String} event, {*} [data], {*} [source]
  2559. * @param {*} [target]
  2560. * @returns {String} id A subscription id
  2561. */
  2562. EventBus.prototype.on = function (event, callback, target) {
  2563. var regexp = (event instanceof RegExp) ?
  2564. event :
  2565. new RegExp(event.replace('*', '\\w+'));
  2566. var subscription = {
  2567. id: util.randomUUID(),
  2568. event: event,
  2569. regexp: regexp,
  2570. callback: (typeof callback === 'function') ? callback : null,
  2571. target: target
  2572. };
  2573. this.subscriptions.push(subscription);
  2574. return subscription.id;
  2575. };
  2576. /**
  2577. * Unsubscribe from an event
  2578. * @param {String | Object} filter Filter for subscriptions to be removed
  2579. * Filter can be a string containing a
  2580. * subscription id, or an object containing
  2581. * one or more of the fields id, event,
  2582. * callback, and target.
  2583. */
  2584. EventBus.prototype.off = function (filter) {
  2585. var i = 0;
  2586. while (i < this.subscriptions.length) {
  2587. var subscription = this.subscriptions[i];
  2588. var match = true;
  2589. if (filter instanceof Object) {
  2590. // filter is an object. All fields must match
  2591. for (var prop in filter) {
  2592. if (filter.hasOwnProperty(prop)) {
  2593. if (filter[prop] !== subscription[prop]) {
  2594. match = false;
  2595. }
  2596. }
  2597. }
  2598. }
  2599. else {
  2600. // filter is a string, filter on id
  2601. match = (subscription.id == filter);
  2602. }
  2603. if (match) {
  2604. this.subscriptions.splice(i, 1);
  2605. }
  2606. else {
  2607. i++;
  2608. }
  2609. }
  2610. };
  2611. /**
  2612. * Emit an event
  2613. * @param {String} event
  2614. * @param {*} [data]
  2615. * @param {*} [source]
  2616. */
  2617. EventBus.prototype.emit = function (event, data, source) {
  2618. for (var i =0; i < this.subscriptions.length; i++) {
  2619. var subscription = this.subscriptions[i];
  2620. if (subscription.regexp.test(event)) {
  2621. if (subscription.callback) {
  2622. subscription.callback(event, data, source);
  2623. }
  2624. }
  2625. }
  2626. };
  2627. /**
  2628. * @constructor Controller
  2629. *
  2630. * A Controller controls the reflows and repaints of all visual components
  2631. */
  2632. function Controller () {
  2633. this.id = util.randomUUID();
  2634. this.components = {};
  2635. this.repaintTimer = undefined;
  2636. this.reflowTimer = undefined;
  2637. }
  2638. /**
  2639. * Add a component to the controller
  2640. * @param {Component | Controller} component
  2641. */
  2642. Controller.prototype.add = function (component) {
  2643. // validate the component
  2644. if (component.id == undefined) {
  2645. throw new Error('Component has no field id');
  2646. }
  2647. if (!(component instanceof Component) && !(component instanceof Controller)) {
  2648. throw new TypeError('Component must be an instance of ' +
  2649. 'prototype Component or Controller');
  2650. }
  2651. // add the component
  2652. component.controller = this;
  2653. this.components[component.id] = component;
  2654. };
  2655. /**
  2656. * Request a reflow. The controller will schedule a reflow
  2657. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  2658. * is false.
  2659. */
  2660. Controller.prototype.requestReflow = function (force) {
  2661. if (force) {
  2662. this.reflow();
  2663. }
  2664. else {
  2665. if (!this.reflowTimer) {
  2666. var me = this;
  2667. this.reflowTimer = setTimeout(function () {
  2668. me.reflowTimer = undefined;
  2669. me.reflow();
  2670. }, 0);
  2671. }
  2672. }
  2673. };
  2674. /**
  2675. * Request a repaint. The controller will schedule a repaint
  2676. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  2677. * is false.
  2678. */
  2679. Controller.prototype.requestRepaint = function (force) {
  2680. if (force) {
  2681. this.repaint();
  2682. }
  2683. else {
  2684. if (!this.repaintTimer) {
  2685. var me = this;
  2686. this.repaintTimer = setTimeout(function () {
  2687. me.repaintTimer = undefined;
  2688. me.repaint();
  2689. }, 0);
  2690. }
  2691. }
  2692. };
  2693. /**
  2694. * Repaint all components
  2695. */
  2696. Controller.prototype.repaint = function () {
  2697. var changed = false;
  2698. // cancel any running repaint request
  2699. if (this.repaintTimer) {
  2700. clearTimeout(this.repaintTimer);
  2701. this.repaintTimer = undefined;
  2702. }
  2703. var done = {};
  2704. function repaint(component, id) {
  2705. if (!(id in done)) {
  2706. // first repaint the components on which this component is dependent
  2707. if (component.depends) {
  2708. component.depends.forEach(function (dep) {
  2709. repaint(dep, dep.id);
  2710. });
  2711. }
  2712. if (component.parent) {
  2713. repaint(component.parent, component.parent.id);
  2714. }
  2715. // repaint the component itself and mark as done
  2716. changed = component.repaint() || changed;
  2717. done[id] = true;
  2718. }
  2719. }
  2720. util.forEach(this.components, repaint);
  2721. // immediately reflow when needed
  2722. if (changed) {
  2723. this.reflow();
  2724. }
  2725. // TODO: limit the number of nested reflows/repaints, prevent loop
  2726. };
  2727. /**
  2728. * Reflow all components
  2729. */
  2730. Controller.prototype.reflow = function () {
  2731. var resized = false;
  2732. // cancel any running repaint request
  2733. if (this.reflowTimer) {
  2734. clearTimeout(this.reflowTimer);
  2735. this.reflowTimer = undefined;
  2736. }
  2737. var done = {};
  2738. function reflow(component, id) {
  2739. if (!(id in done)) {
  2740. // first reflow the components on which this component is dependent
  2741. if (component.depends) {
  2742. component.depends.forEach(function (dep) {
  2743. reflow(dep, dep.id);
  2744. });
  2745. }
  2746. if (component.parent) {
  2747. reflow(component.parent, component.parent.id);
  2748. }
  2749. // reflow the component itself and mark as done
  2750. resized = component.reflow() || resized;
  2751. done[id] = true;
  2752. }
  2753. }
  2754. util.forEach(this.components, reflow);
  2755. // immediately repaint when needed
  2756. if (resized) {
  2757. this.repaint();
  2758. }
  2759. // TODO: limit the number of nested reflows/repaints, prevent loop
  2760. };
  2761. /**
  2762. * Prototype for visual components
  2763. */
  2764. function Component () {
  2765. this.id = null;
  2766. this.parent = null;
  2767. this.depends = null;
  2768. this.controller = null;
  2769. this.options = null;
  2770. this.frame = null; // main DOM element
  2771. this.top = 0;
  2772. this.left = 0;
  2773. this.width = 0;
  2774. this.height = 0;
  2775. }
  2776. /**
  2777. * Set parameters for the frame. Parameters will be merged in current parameter
  2778. * set.
  2779. * @param {Object} options Available parameters:
  2780. * {String | function} [className]
  2781. * {EventBus} [eventBus]
  2782. * {String | Number | function} [left]
  2783. * {String | Number | function} [top]
  2784. * {String | Number | function} [width]
  2785. * {String | Number | function} [height]
  2786. */
  2787. Component.prototype.setOptions = function(options) {
  2788. if (!options) {
  2789. return;
  2790. }
  2791. util.extend(this.options, options);
  2792. if (this.controller) {
  2793. this.requestRepaint();
  2794. this.requestReflow();
  2795. }
  2796. };
  2797. /**
  2798. * Get the container element of the component, which can be used by a child to
  2799. * add its own widgets. Not all components do have a container for childs, in
  2800. * that case null is returned.
  2801. * @returns {HTMLElement | null} container
  2802. */
  2803. Component.prototype.getContainer = function () {
  2804. // should be implemented by the component
  2805. return null;
  2806. };
  2807. /**
  2808. * Get the frame element of the component, the outer HTML DOM element.
  2809. * @returns {HTMLElement | null} frame
  2810. */
  2811. Component.prototype.getFrame = function () {
  2812. return this.frame;
  2813. };
  2814. /**
  2815. * Repaint the component
  2816. * @return {Boolean} changed
  2817. */
  2818. Component.prototype.repaint = function () {
  2819. // should be implemented by the component
  2820. return false;
  2821. };
  2822. /**
  2823. * Reflow the component
  2824. * @return {Boolean} resized
  2825. */
  2826. Component.prototype.reflow = function () {
  2827. // should be implemented by the component
  2828. return false;
  2829. };
  2830. /**
  2831. * Request a repaint. The controller will schedule a repaint
  2832. */
  2833. Component.prototype.requestRepaint = function () {
  2834. if (this.controller) {
  2835. this.controller.requestRepaint();
  2836. }
  2837. else {
  2838. throw new Error('Cannot request a repaint: no controller configured');
  2839. // TODO: just do a repaint when no parent is configured?
  2840. }
  2841. };
  2842. /**
  2843. * Request a reflow. The controller will schedule a reflow
  2844. */
  2845. Component.prototype.requestReflow = function () {
  2846. if (this.controller) {
  2847. this.controller.requestReflow();
  2848. }
  2849. else {
  2850. throw new Error('Cannot request a reflow: no controller configured');
  2851. // TODO: just do a reflow when no parent is configured?
  2852. }
  2853. };
  2854. /**
  2855. * A panel can contain components
  2856. * @param {Component} [parent]
  2857. * @param {Component[]} [depends] Components on which this components depends
  2858. * (except for the parent)
  2859. * @param {Object} [options] Available parameters:
  2860. * {String | Number | function} [left]
  2861. * {String | Number | function} [top]
  2862. * {String | Number | function} [width]
  2863. * {String | Number | function} [height]
  2864. * {String | function} [className]
  2865. * @constructor Panel
  2866. * @extends Component
  2867. */
  2868. function Panel(parent, depends, options) {
  2869. this.id = util.randomUUID();
  2870. this.parent = parent;
  2871. this.depends = depends;
  2872. this.options = {};
  2873. this.setOptions(options);
  2874. }
  2875. Panel.prototype = new Component();
  2876. /**
  2877. * Get the container element of the panel, which can be used by a child to
  2878. * add its own widgets.
  2879. * @returns {HTMLElement} container
  2880. */
  2881. Panel.prototype.getContainer = function () {
  2882. return this.frame;
  2883. };
  2884. /**
  2885. * Repaint the component
  2886. * @return {Boolean} changed
  2887. */
  2888. Panel.prototype.repaint = function () {
  2889. var changed = 0,
  2890. update = util.updateProperty,
  2891. asSize = util.option.asSize,
  2892. options = this.options,
  2893. frame = this.frame;
  2894. if (!frame) {
  2895. frame = document.createElement('div');
  2896. frame.className = 'panel';
  2897. if (options.className) {
  2898. if (typeof options.className == 'function') {
  2899. util.addClassName(frame, String(options.className()));
  2900. }
  2901. else {
  2902. util.addClassName(frame, String(options.className));
  2903. }
  2904. }
  2905. this.frame = frame;
  2906. changed += 1;
  2907. }
  2908. if (!frame.parentNode) {
  2909. if (!this.parent) {
  2910. throw new Error('Cannot repaint panel: no parent attached');
  2911. }
  2912. var parentContainer = this.parent.getContainer();
  2913. if (!parentContainer) {
  2914. throw new Error('Cannot repaint panel: parent has no container element');
  2915. }
  2916. parentContainer.appendChild(frame);
  2917. changed += 1;
  2918. }
  2919. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  2920. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  2921. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  2922. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  2923. return (changed > 0);
  2924. };
  2925. /**
  2926. * Reflow the component
  2927. * @return {Boolean} resized
  2928. */
  2929. Panel.prototype.reflow = function () {
  2930. var changed = 0,
  2931. update = util.updateProperty,
  2932. frame = this.frame;
  2933. if (frame) {
  2934. changed += update(this, 'top', frame.offsetTop);
  2935. changed += update(this, 'left', frame.offsetLeft);
  2936. changed += update(this, 'width', frame.offsetWidth);
  2937. changed += update(this, 'height', frame.offsetHeight);
  2938. }
  2939. else {
  2940. changed += 1;
  2941. }
  2942. return (changed > 0);
  2943. };
  2944. /**
  2945. * A root panel can hold components. The root panel must be initialized with
  2946. * a DOM element as container.
  2947. * @param {HTMLElement} container
  2948. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  2949. * @constructor RootPanel
  2950. * @extends Panel
  2951. */
  2952. function RootPanel(container, options) {
  2953. this.id = util.randomUUID();
  2954. this.container = container;
  2955. this.options = {
  2956. autoResize: true
  2957. };
  2958. this.listeners = {}; // event listeners
  2959. this.setOptions(options);
  2960. }
  2961. RootPanel.prototype = new Panel();
  2962. /**
  2963. * Set options. Will extend the current options.
  2964. * @param {Object} [options] Available parameters:
  2965. * {String | function} [className]
  2966. * {String | Number | function} [left]
  2967. * {String | Number | function} [top]
  2968. * {String | Number | function} [width]
  2969. * {String | Number | function} [height]
  2970. * {String | Number | function} [height]
  2971. * {Boolean | function} [autoResize]
  2972. */
  2973. RootPanel.prototype.setOptions = function (options) {
  2974. util.extend(this.options, options);
  2975. if (this.options.autoResize) {
  2976. this._watch();
  2977. }
  2978. else {
  2979. this._unwatch();
  2980. }
  2981. };
  2982. /**
  2983. * Repaint the component
  2984. * @return {Boolean} changed
  2985. */
  2986. RootPanel.prototype.repaint = function () {
  2987. var changed = 0,
  2988. update = util.updateProperty,
  2989. asSize = util.option.asSize,
  2990. options = this.options,
  2991. frame = this.frame;
  2992. if (!frame) {
  2993. frame = document.createElement('div');
  2994. frame.className = 'graph panel';
  2995. if (options.className) {
  2996. util.addClassName(frame, util.option.asString(options.className));
  2997. }
  2998. this.frame = frame;
  2999. changed += 1;
  3000. }
  3001. if (!frame.parentNode) {
  3002. if (!this.container) {
  3003. throw new Error('Cannot repaint root panel: no container attached');
  3004. }
  3005. this.container.appendChild(frame);
  3006. changed += 1;
  3007. }
  3008. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3009. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3010. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3011. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3012. this._updateEventEmitters();
  3013. return (changed > 0);
  3014. };
  3015. /**
  3016. * Reflow the component
  3017. * @return {Boolean} resized
  3018. */
  3019. RootPanel.prototype.reflow = function () {
  3020. var changed = 0,
  3021. update = util.updateProperty,
  3022. frame = this.frame;
  3023. if (frame) {
  3024. changed += update(this, 'top', frame.offsetTop);
  3025. changed += update(this, 'left', frame.offsetLeft);
  3026. changed += update(this, 'width', frame.offsetWidth);
  3027. changed += update(this, 'height', frame.offsetHeight);
  3028. }
  3029. else {
  3030. changed += 1;
  3031. }
  3032. return (changed > 0);
  3033. };
  3034. /**
  3035. * Watch for changes in the size of the frame. On resize, the Panel will
  3036. * automatically redraw itself.
  3037. * @private
  3038. */
  3039. RootPanel.prototype._watch = function () {
  3040. var me = this;
  3041. this._unwatch();
  3042. var checkSize = function () {
  3043. if (!me.options.autoResize) {
  3044. // stop watching when the option autoResize is changed to false
  3045. me._unwatch();
  3046. return;
  3047. }
  3048. if (me.frame) {
  3049. // check whether the frame is resized
  3050. if ((me.frame.clientWidth != me.width) ||
  3051. (me.frame.clientHeight != me.height)) {
  3052. me.requestReflow();
  3053. }
  3054. }
  3055. };
  3056. // TODO: automatically cleanup the event listener when the frame is deleted
  3057. util.addEventListener(window, 'resize', checkSize);
  3058. this.watchTimer = setInterval(checkSize, 1000);
  3059. };
  3060. /**
  3061. * Stop watching for a resize of the frame.
  3062. * @private
  3063. */
  3064. RootPanel.prototype._unwatch = function () {
  3065. if (this.watchTimer) {
  3066. clearInterval(this.watchTimer);
  3067. this.watchTimer = undefined;
  3068. }
  3069. // TODO: remove event listener on window.resize
  3070. };
  3071. /**
  3072. * Event handler
  3073. * @param {String} event name of the event, for example 'click', 'mousemove'
  3074. * @param {function} callback callback handler, invoked with the raw HTML Event
  3075. * as parameter.
  3076. */
  3077. RootPanel.prototype.on = function (event, callback) {
  3078. // register the listener at this component
  3079. var arr = this.listeners[event];
  3080. if (!arr) {
  3081. arr = [];
  3082. this.listeners[event] = arr;
  3083. }
  3084. arr.push(callback);
  3085. this._updateEventEmitters();
  3086. };
  3087. /**
  3088. * Update the event listeners for all event emitters
  3089. * @private
  3090. */
  3091. RootPanel.prototype._updateEventEmitters = function () {
  3092. if (this.listeners) {
  3093. var me = this;
  3094. util.forEach(this.listeners, function (listeners, event) {
  3095. if (!me.emitters) {
  3096. me.emitters = {};
  3097. }
  3098. if (!(event in me.emitters)) {
  3099. // create event
  3100. var frame = me.frame;
  3101. if (frame) {
  3102. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3103. var callback = function(event) {
  3104. listeners.forEach(function (listener) {
  3105. // TODO: filter on event target!
  3106. listener(event);
  3107. });
  3108. };
  3109. me.emitters[event] = callback;
  3110. util.addEventListener(frame, event, callback);
  3111. }
  3112. }
  3113. });
  3114. // TODO: be able to delete event listeners
  3115. // TODO: be able to move event listeners to a parent when available
  3116. }
  3117. };
  3118. /**
  3119. * A horizontal time axis
  3120. * @param {Component} parent
  3121. * @param {Component[]} [depends] Components on which this components depends
  3122. * (except for the parent)
  3123. * @param {Object} [options] See TimeAxis.setOptions for the available
  3124. * options.
  3125. * @constructor TimeAxis
  3126. * @extends Component
  3127. */
  3128. function TimeAxis (parent, depends, options) {
  3129. this.id = util.randomUUID();
  3130. this.parent = parent;
  3131. this.depends = depends;
  3132. this.dom = {
  3133. majorLines: [],
  3134. majorTexts: [],
  3135. minorLines: [],
  3136. minorTexts: [],
  3137. redundant: {
  3138. majorLines: [],
  3139. majorTexts: [],
  3140. minorLines: [],
  3141. minorTexts: []
  3142. }
  3143. };
  3144. this.props = {
  3145. range: {
  3146. start: 0,
  3147. end: 0,
  3148. minimumStep: 0
  3149. },
  3150. lineTop: 0
  3151. };
  3152. this.options = {
  3153. orientation: 'bottom', // supported: 'top', 'bottom'
  3154. // TODO: implement timeaxis orientations 'left' and 'right'
  3155. showMinorLabels: true,
  3156. showMajorLabels: true
  3157. };
  3158. this.conversion = null;
  3159. this.range = null;
  3160. this.setOptions(options);
  3161. }
  3162. TimeAxis.prototype = new Component();
  3163. // TODO: comment options
  3164. TimeAxis.prototype.setOptions = function (options) {
  3165. util.extend(this.options, options);
  3166. };
  3167. /**
  3168. * Set a range (start and end)
  3169. * @param {Range | Object} range A Range or an object containing start and end.
  3170. */
  3171. TimeAxis.prototype.setRange = function (range) {
  3172. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3173. throw new TypeError('Range must be an instance of Range, ' +
  3174. 'or an object containing start and end.');
  3175. }
  3176. this.range = range;
  3177. };
  3178. /**
  3179. * Convert a position on screen (pixels) to a datetime
  3180. * @param {int} x Position on the screen in pixels
  3181. * @return {Date} time The datetime the corresponds with given position x
  3182. */
  3183. TimeAxis.prototype.toTime = function(x) {
  3184. var conversion = this.conversion;
  3185. return new Date(x / conversion.factor + conversion.offset);
  3186. };
  3187. /**
  3188. * Convert a datetime (Date object) into a position on the screen
  3189. * @param {Date} time A date
  3190. * @return {int} x The position on the screen in pixels which corresponds
  3191. * with the given date.
  3192. * @private
  3193. */
  3194. TimeAxis.prototype.toScreen = function(time) {
  3195. var conversion = this.conversion;
  3196. return (time.valueOf() - conversion.offset) * conversion.factor;
  3197. };
  3198. /**
  3199. * Repaint the component
  3200. * @return {Boolean} changed
  3201. */
  3202. TimeAxis.prototype.repaint = function () {
  3203. var changed = 0,
  3204. update = util.updateProperty,
  3205. asSize = util.option.asSize,
  3206. options = this.options,
  3207. props = this.props,
  3208. step = this.step;
  3209. var frame = this.frame;
  3210. if (!frame) {
  3211. frame = document.createElement('div');
  3212. this.frame = frame;
  3213. changed += 1;
  3214. }
  3215. frame.className = 'axis ' + options.orientation;
  3216. // TODO: custom className?
  3217. if (!frame.parentNode) {
  3218. if (!this.parent) {
  3219. throw new Error('Cannot repaint time axis: no parent attached');
  3220. }
  3221. var parentContainer = this.parent.getContainer();
  3222. if (!parentContainer) {
  3223. throw new Error('Cannot repaint time axis: parent has no container element');
  3224. }
  3225. parentContainer.appendChild(frame);
  3226. changed += 1;
  3227. }
  3228. var parent = frame.parentNode;
  3229. if (parent) {
  3230. var beforeChild = frame.nextSibling;
  3231. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3232. var orientation = options.orientation;
  3233. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3234. (this.props.parentHeight - this.height) + 'px' :
  3235. '0px';
  3236. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3237. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3238. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3239. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3240. // get characters width and height
  3241. this._repaintMeasureChars();
  3242. if (this.step) {
  3243. this._repaintStart();
  3244. step.first();
  3245. var xFirstMajorLabel = undefined;
  3246. var max = 0;
  3247. while (step.hasNext() && max < 1000) {
  3248. max++;
  3249. var cur = step.getCurrent(),
  3250. x = this.toScreen(cur),
  3251. isMajor = step.isMajor();
  3252. // TODO: lines must have a width, such that we can create css backgrounds
  3253. if (options.showMinorLabels) {
  3254. this._repaintMinorText(x, step.getLabelMinor());
  3255. }
  3256. if (isMajor && options.showMajorLabels) {
  3257. if (x > 0) {
  3258. if (xFirstMajorLabel == undefined) {
  3259. xFirstMajorLabel = x;
  3260. }
  3261. this._repaintMajorText(x, step.getLabelMajor());
  3262. }
  3263. this._repaintMajorLine(x);
  3264. }
  3265. else {
  3266. this._repaintMinorLine(x);
  3267. }
  3268. step.next();
  3269. }
  3270. // create a major label on the left when needed
  3271. if (options.showMajorLabels) {
  3272. var leftTime = this.toTime(0),
  3273. leftText = step.getLabelMajor(leftTime),
  3274. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3275. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3276. this._repaintMajorText(0, leftText);
  3277. }
  3278. }
  3279. this._repaintEnd();
  3280. }
  3281. this._repaintLine();
  3282. // put frame online again
  3283. if (beforeChild) {
  3284. parent.insertBefore(frame, beforeChild);
  3285. }
  3286. else {
  3287. parent.appendChild(frame)
  3288. }
  3289. }
  3290. return (changed > 0);
  3291. };
  3292. /**
  3293. * Start a repaint. Move all DOM elements to a redundant list, where they
  3294. * can be picked for re-use, or can be cleaned up in the end
  3295. * @private
  3296. */
  3297. TimeAxis.prototype._repaintStart = function () {
  3298. var dom = this.dom,
  3299. redundant = dom.redundant;
  3300. redundant.majorLines = dom.majorLines;
  3301. redundant.majorTexts = dom.majorTexts;
  3302. redundant.minorLines = dom.minorLines;
  3303. redundant.minorTexts = dom.minorTexts;
  3304. dom.majorLines = [];
  3305. dom.majorTexts = [];
  3306. dom.minorLines = [];
  3307. dom.minorTexts = [];
  3308. };
  3309. /**
  3310. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3311. * @private
  3312. */
  3313. TimeAxis.prototype._repaintEnd = function () {
  3314. util.forEach(this.dom.redundant, function (arr) {
  3315. while (arr.length) {
  3316. var elem = arr.pop();
  3317. if (elem && elem.parentNode) {
  3318. elem.parentNode.removeChild(elem);
  3319. }
  3320. }
  3321. });
  3322. };
  3323. /**
  3324. * Create a minor label for the axis at position x
  3325. * @param {Number} x
  3326. * @param {String} text
  3327. * @private
  3328. */
  3329. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3330. // reuse redundant label
  3331. var label = this.dom.redundant.minorTexts.shift();
  3332. if (!label) {
  3333. // create new label
  3334. var content = document.createTextNode('');
  3335. label = document.createElement('div');
  3336. label.appendChild(content);
  3337. label.className = 'text minor';
  3338. this.frame.appendChild(label);
  3339. }
  3340. this.dom.minorTexts.push(label);
  3341. label.childNodes[0].nodeValue = text;
  3342. label.style.left = x + 'px';
  3343. label.style.top = this.props.minorLabelTop + 'px';
  3344. //label.title = title; // TODO: this is a heavy operation
  3345. };
  3346. /**
  3347. * Create a Major label for the axis at position x
  3348. * @param {Number} x
  3349. * @param {String} text
  3350. * @private
  3351. */
  3352. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3353. // reuse redundant label
  3354. var label = this.dom.redundant.majorTexts.shift();
  3355. if (!label) {
  3356. // create label
  3357. var content = document.createTextNode(text);
  3358. label = document.createElement('div');
  3359. label.className = 'text major';
  3360. label.appendChild(content);
  3361. this.frame.appendChild(label);
  3362. }
  3363. this.dom.majorTexts.push(label);
  3364. label.childNodes[0].nodeValue = text;
  3365. label.style.top = this.props.majorLabelTop + 'px';
  3366. label.style.left = x + 'px';
  3367. //label.title = title; // TODO: this is a heavy operation
  3368. };
  3369. /**
  3370. * Create a minor line for the axis at position x
  3371. * @param {Number} x
  3372. * @private
  3373. */
  3374. TimeAxis.prototype._repaintMinorLine = function (x) {
  3375. // reuse redundant line
  3376. var line = this.dom.redundant.minorLines.shift();
  3377. if (!line) {
  3378. // create vertical line
  3379. line = document.createElement('div');
  3380. line.className = 'grid vertical minor';
  3381. this.frame.appendChild(line);
  3382. }
  3383. this.dom.minorLines.push(line);
  3384. var props = this.props;
  3385. line.style.top = props.minorLineTop + 'px';
  3386. line.style.height = props.minorLineHeight + 'px';
  3387. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3388. };
  3389. /**
  3390. * Create a Major line for the axis at position x
  3391. * @param {Number} x
  3392. * @private
  3393. */
  3394. TimeAxis.prototype._repaintMajorLine = function (x) {
  3395. // reuse redundant line
  3396. var line = this.dom.redundant.majorLines.shift();
  3397. if (!line) {
  3398. // create vertical line
  3399. line = document.createElement('DIV');
  3400. line.className = 'grid vertical major';
  3401. this.frame.appendChild(line);
  3402. }
  3403. this.dom.majorLines.push(line);
  3404. var props = this.props;
  3405. line.style.top = props.majorLineTop + 'px';
  3406. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3407. line.style.height = props.majorLineHeight + 'px';
  3408. };
  3409. /**
  3410. * Repaint the horizontal line for the axis
  3411. * @private
  3412. */
  3413. TimeAxis.prototype._repaintLine = function() {
  3414. var line = this.dom.line,
  3415. frame = this.frame,
  3416. options = this.options;
  3417. // line before all axis elements
  3418. if (options.showMinorLabels || options.showMajorLabels) {
  3419. if (line) {
  3420. // put this line at the end of all childs
  3421. frame.removeChild(line);
  3422. frame.appendChild(line);
  3423. }
  3424. else {
  3425. // create the axis line
  3426. line = document.createElement('div');
  3427. line.className = 'grid horizontal major';
  3428. frame.appendChild(line);
  3429. this.dom.line = line;
  3430. }
  3431. line.style.top = this.props.lineTop + 'px';
  3432. }
  3433. else {
  3434. if (line && axis.parentElement) {
  3435. frame.removeChild(axis.line);
  3436. delete this.dom.line;
  3437. }
  3438. }
  3439. };
  3440. /**
  3441. * Create characters used to determine the size of text on the axis
  3442. * @private
  3443. */
  3444. TimeAxis.prototype._repaintMeasureChars = function () {
  3445. // calculate the width and height of a single character
  3446. // this is used to calculate the step size, and also the positioning of the
  3447. // axis
  3448. var dom = this.dom,
  3449. text;
  3450. if (!dom.characterMinor) {
  3451. text = document.createTextNode('0');
  3452. var measureCharMinor = document.createElement('DIV');
  3453. measureCharMinor.className = 'text minor measure';
  3454. measureCharMinor.appendChild(text);
  3455. this.frame.appendChild(measureCharMinor);
  3456. dom.measureCharMinor = measureCharMinor;
  3457. }
  3458. if (!dom.characterMajor) {
  3459. text = document.createTextNode('0');
  3460. var measureCharMajor = document.createElement('DIV');
  3461. measureCharMajor.className = 'text major measure';
  3462. measureCharMajor.appendChild(text);
  3463. this.frame.appendChild(measureCharMajor);
  3464. dom.measureCharMajor = measureCharMajor;
  3465. }
  3466. };
  3467. /**
  3468. * Reflow the component
  3469. * @return {Boolean} resized
  3470. */
  3471. TimeAxis.prototype.reflow = function () {
  3472. var changed = 0,
  3473. update = util.updateProperty,
  3474. frame = this.frame,
  3475. range = this.range;
  3476. if (!range) {
  3477. throw new Error('Cannot repaint time axis: no range configured');
  3478. }
  3479. if (frame) {
  3480. changed += update(this, 'top', frame.offsetTop);
  3481. changed += update(this, 'left', frame.offsetLeft);
  3482. // calculate size of a character
  3483. var props = this.props,
  3484. showMinorLabels = this.options.showMinorLabels,
  3485. showMajorLabels = this.options.showMajorLabels,
  3486. measureCharMinor = this.dom.measureCharMinor,
  3487. measureCharMajor = this.dom.measureCharMajor;
  3488. if (measureCharMinor) {
  3489. props.minorCharHeight = measureCharMinor.clientHeight;
  3490. props.minorCharWidth = measureCharMinor.clientWidth;
  3491. }
  3492. if (measureCharMajor) {
  3493. props.majorCharHeight = measureCharMajor.clientHeight;
  3494. props.majorCharWidth = measureCharMajor.clientWidth;
  3495. }
  3496. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  3497. if (parentHeight != props.parentHeight) {
  3498. props.parentHeight = parentHeight;
  3499. changed += 1;
  3500. }
  3501. switch (this.options.orientation) {
  3502. case 'bottom':
  3503. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3504. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3505. props.minorLabelTop = 0;
  3506. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  3507. props.minorLineTop = -this.top;
  3508. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  3509. props.minorLineWidth = 1; // TODO: really calculate width
  3510. props.majorLineTop = -this.top;
  3511. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  3512. props.majorLineWidth = 1; // TODO: really calculate width
  3513. props.lineTop = 0;
  3514. break;
  3515. case 'top':
  3516. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3517. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3518. props.majorLabelTop = 0;
  3519. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  3520. props.minorLineTop = props.minorLabelTop;
  3521. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  3522. props.minorLineWidth = 1; // TODO: really calculate width
  3523. props.majorLineTop = 0;
  3524. props.majorLineHeight = Math.max(parentHeight - this.top);
  3525. props.majorLineWidth = 1; // TODO: really calculate width
  3526. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  3527. break;
  3528. default:
  3529. throw new Error('Unkown orientation "' + this.options.orientation + '"');
  3530. }
  3531. var height = props.minorLabelHeight + props.majorLabelHeight;
  3532. changed += update(this, 'width', frame.offsetWidth);
  3533. changed += update(this, 'height', height);
  3534. // calculate range and step
  3535. this._updateConversion();
  3536. var start = util.cast(range.start, 'Date'),
  3537. end = util.cast(range.end, 'Date'),
  3538. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  3539. this.step = new TimeStep(start, end, minimumStep);
  3540. changed += update(props.range, 'start', start.valueOf());
  3541. changed += update(props.range, 'end', end.valueOf());
  3542. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  3543. }
  3544. return (changed > 0);
  3545. };
  3546. /**
  3547. * Calculate the factor and offset to convert a position on screen to the
  3548. * corresponding date and vice versa.
  3549. * After the method _updateConversion is executed once, the methods toTime
  3550. * and toScreen can be used.
  3551. * @private
  3552. */
  3553. TimeAxis.prototype._updateConversion = function() {
  3554. var range = this.range;
  3555. if (!range) {
  3556. throw new Error('No range configured');
  3557. }
  3558. if (range.conversion) {
  3559. this.conversion = range.conversion(this.width);
  3560. }
  3561. else {
  3562. this.conversion = Range.conversion(range.start, range.end, this.width);
  3563. }
  3564. };
  3565. /**
  3566. * An ItemSet holds a set of items and ranges which can be displayed in a
  3567. * range. The width is determined by the parent of the ItemSet, and the height
  3568. * is determined by the size of the items.
  3569. * @param {Component} parent
  3570. * @param {Component[]} [depends] Components on which this components depends
  3571. * (except for the parent)
  3572. * @param {Object} [options] See ItemSet.setOptions for the available
  3573. * options.
  3574. * @constructor ItemSet
  3575. * @extends Panel
  3576. */
  3577. // TODO: improve performance by replacing all Array.forEach with a for loop
  3578. function ItemSet(parent, depends, options) {
  3579. this.id = util.randomUUID();
  3580. this.parent = parent;
  3581. this.depends = depends;
  3582. // one options object is shared by this itemset and all its items
  3583. this.options = {
  3584. style: 'box',
  3585. align: 'center',
  3586. orientation: 'bottom',
  3587. margin: {
  3588. axis: 20,
  3589. item: 10
  3590. },
  3591. padding: 5
  3592. };
  3593. this.dom = {};
  3594. var me = this;
  3595. this.data = null; // DataSet
  3596. this.range = null; // Range or Object {start: number, end: number}
  3597. this.listeners = {
  3598. 'add': function (event, params) {
  3599. me._onAdd(params.items);
  3600. },
  3601. 'update': function (event, params) {
  3602. me._onUpdate(params.items);
  3603. },
  3604. 'remove': function (event, params) {
  3605. me._onRemove(params.items);
  3606. }
  3607. };
  3608. this.items = {};
  3609. this.queue = {}; // queue with items to be added/updated/removed
  3610. this.stack = new Stack(this);
  3611. this.conversion = null;
  3612. this.setOptions(options);
  3613. }
  3614. ItemSet.prototype = new Panel();
  3615. // available item types will be registered here
  3616. ItemSet.types = {
  3617. box: ItemBox,
  3618. range: ItemRange,
  3619. point: ItemPoint
  3620. };
  3621. /**
  3622. * Set options for the ItemSet. Existing options will be extended/overwritten.
  3623. * @param {Object} [options] The following options are available:
  3624. * {String | function} [className]
  3625. * class name for the itemset
  3626. * {String} [style]
  3627. * Default style for the items. Choose from 'box'
  3628. * (default), 'point', or 'range'. The default
  3629. * Style can be overwritten by individual items.
  3630. * {String} align
  3631. * Alignment for the items, only applicable for
  3632. * ItemBox. Choose 'center' (default), 'left', or
  3633. * 'right'.
  3634. * {String} orientation
  3635. * Orientation of the item set. Choose 'top' or
  3636. * 'bottom' (default).
  3637. * {Number} margin.axis
  3638. * Margin between the axis and the items in pixels.
  3639. * Default is 20.
  3640. * {Number} margin.item
  3641. * Margin between items in pixels. Default is 10.
  3642. * {Number} padding
  3643. * Padding of the contents of an item in pixels.
  3644. * Must correspond with the items css. Default is 5.
  3645. */
  3646. ItemSet.prototype.setOptions = function setOptions(options) {
  3647. util.extend(this.options, options);
  3648. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  3649. this.stack.setOptions(this.options);
  3650. };
  3651. /**
  3652. * Set range (start and end).
  3653. * @param {Range | Object} range A Range or an object containing start and end.
  3654. */
  3655. ItemSet.prototype.setRange = function setRange(range) {
  3656. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3657. throw new TypeError('Range must be an instance of Range, ' +
  3658. 'or an object containing start and end.');
  3659. }
  3660. this.range = range;
  3661. };
  3662. /**
  3663. * Repaint the component
  3664. * @return {Boolean} changed
  3665. */
  3666. ItemSet.prototype.repaint = function repaint() {
  3667. var changed = 0,
  3668. update = util.updateProperty,
  3669. asSize = util.option.asSize,
  3670. options = this.options,
  3671. frame = this.frame;
  3672. if (!frame) {
  3673. frame = document.createElement('div');
  3674. frame.className = 'itemset';
  3675. if (options.className) {
  3676. util.addClassName(frame, util.option.asString(options.className));
  3677. }
  3678. // create background panel
  3679. var background = document.createElement('div');
  3680. background.className = 'background';
  3681. frame.appendChild(background);
  3682. this.dom.background = background;
  3683. // create foreground panel
  3684. var foreground = document.createElement('div');
  3685. foreground.className = 'foreground';
  3686. frame.appendChild(foreground);
  3687. this.dom.foreground = foreground;
  3688. // create axis panel
  3689. var axis = document.createElement('div');
  3690. axis.className = 'itemset-axis';
  3691. //frame.appendChild(axis);
  3692. this.dom.axis = axis;
  3693. this.frame = frame;
  3694. changed += 1;
  3695. }
  3696. if (!this.parent) {
  3697. throw new Error('Cannot repaint itemset: no parent attached');
  3698. }
  3699. var parentContainer = this.parent.getContainer();
  3700. if (!parentContainer) {
  3701. throw new Error('Cannot repaint itemset: parent has no container element');
  3702. }
  3703. if (!frame.parentNode) {
  3704. parentContainer.appendChild(frame);
  3705. changed += 1;
  3706. }
  3707. if (!this.dom.axis.parentNode) {
  3708. parentContainer.appendChild(this.dom.axis);
  3709. changed += 1;
  3710. }
  3711. // reposition frame
  3712. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3713. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3714. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3715. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3716. // reposition axis
  3717. changed += update(this.dom.axis.style, 'top', asSize(options.top, '0px'));
  3718. this._updateConversion();
  3719. var me = this,
  3720. queue = this.queue,
  3721. data = this.data,
  3722. items = this.items,
  3723. dataOptions = {
  3724. fields: ['id', 'start', 'end', 'content', 'type']
  3725. };
  3726. // TODO: copy options from the itemset itself?
  3727. // TODO: make orientation dynamically changable for the items
  3728. // show/hide added/changed/removed items
  3729. Object.keys(queue).forEach(function (id) {
  3730. var entry = queue[id];
  3731. var item = entry.item;
  3732. //noinspection FallthroughInSwitchStatementJS
  3733. switch (entry.action) {
  3734. case 'add':
  3735. case 'update':
  3736. var itemData = data.get(id, dataOptions);
  3737. var type = itemData.type ||
  3738. (itemData.start && itemData.end && 'range') ||
  3739. 'box';
  3740. var constructor = ItemSet.types[type];
  3741. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  3742. if (item) {
  3743. // update item
  3744. if (!constructor || !(item instanceof constructor)) {
  3745. // item type has changed, hide and delete the item
  3746. changed += item.hide();
  3747. item = null;
  3748. }
  3749. else {
  3750. item.data = itemData; // TODO: create a method item.setData ?
  3751. changed++;
  3752. }
  3753. }
  3754. if (!item) {
  3755. // create item
  3756. if (constructor) {
  3757. item = new constructor(me, itemData, options);
  3758. changed++;
  3759. }
  3760. else {
  3761. throw new TypeError('Unknown item type "' + type + '"');
  3762. }
  3763. }
  3764. // update lists
  3765. items[id] = item;
  3766. delete queue[id];
  3767. break;
  3768. case 'remove':
  3769. if (item) {
  3770. // remove DOM of the item
  3771. changed += item.hide();
  3772. }
  3773. // update lists
  3774. delete items[id];
  3775. delete queue[id];
  3776. break;
  3777. default:
  3778. console.log('Error: unknown action "' + entry.action + '"');
  3779. }
  3780. });
  3781. // reposition all items. Show items only when in the visible area
  3782. util.forEach(this.items, function (item) {
  3783. if (item.visible) {
  3784. changed += item.show();
  3785. item.reposition();
  3786. }
  3787. else {
  3788. changed += item.hide();
  3789. }
  3790. });
  3791. return (changed > 0);
  3792. };
  3793. /**
  3794. * Get the foreground container element
  3795. * @return {HTMLElement} foreground
  3796. */
  3797. ItemSet.prototype.getForeground = function getForeground() {
  3798. return this.dom.foreground;
  3799. };
  3800. /**
  3801. * Get the background container element
  3802. * @return {HTMLElement} background
  3803. */
  3804. ItemSet.prototype.getBackground = function getBackground() {
  3805. return this.dom.background;
  3806. };
  3807. /**
  3808. * Get the axis container element
  3809. * @return {HTMLElement} axis
  3810. */
  3811. ItemSet.prototype.getAxis = function getAxis() {
  3812. return this.dom.axis;
  3813. };
  3814. /**
  3815. * Reflow the component
  3816. * @return {Boolean} resized
  3817. */
  3818. ItemSet.prototype.reflow = function reflow () {
  3819. var changed = 0,
  3820. options = this.options,
  3821. update = util.updateProperty,
  3822. asNumber = util.option.asNumber,
  3823. frame = this.frame;
  3824. if (frame) {
  3825. this._updateConversion();
  3826. util.forEach(this.items, function (item) {
  3827. changed += item.reflow();
  3828. });
  3829. // TODO: stack.update should be triggered via an event, in stack itself
  3830. // TODO: only update the stack when there are changed items
  3831. this.stack.update();
  3832. var maxHeight = asNumber(options.maxHeight);
  3833. var height;
  3834. if (options.height != null) {
  3835. height = frame.offsetHeight;
  3836. }
  3837. else {
  3838. // height is not specified, determine the height from the height and positioned items
  3839. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  3840. if (visibleItems.length) {
  3841. var min = visibleItems[0].top;
  3842. var max = visibleItems[0].top + visibleItems[0].height;
  3843. util.forEach(visibleItems, function (item) {
  3844. min = Math.min(min, item.top);
  3845. max = Math.max(max, (item.top + item.height));
  3846. });
  3847. height = (max - min) + options.margin.axis + options.margin.item;
  3848. }
  3849. else {
  3850. height = options.margin.axis + options.margin.item;
  3851. }
  3852. }
  3853. if (maxHeight != null) {
  3854. height = Math.min(height, maxHeight);
  3855. }
  3856. changed += update(this, 'height', height);
  3857. // calculate height from items
  3858. changed += update(this, 'top', frame.offsetTop);
  3859. changed += update(this, 'left', frame.offsetLeft);
  3860. changed += update(this, 'width', frame.offsetWidth);
  3861. }
  3862. else {
  3863. changed += 1;
  3864. }
  3865. return (changed > 0);
  3866. };
  3867. /**
  3868. * Set data
  3869. * @param {DataSet | Array | DataTable} data
  3870. */
  3871. ItemSet.prototype.setData = function setData(data) {
  3872. var me = this,
  3873. dataItems,
  3874. ids;
  3875. // unsubscribe from current dataset
  3876. var current = this.data;
  3877. if (current) {
  3878. util.forEach(this.listeners, function (callback, event) {
  3879. current.unsubscribe(event, callback);
  3880. });
  3881. // remove all drawn items
  3882. dataItems = current.get({fields: ['id']});
  3883. ids = [];
  3884. util.forEach(dataItems, function (dataItem, index) {
  3885. ids[index] = dataItem.id;
  3886. });
  3887. this._onRemove(ids);
  3888. }
  3889. // replace the dataset
  3890. if (data instanceof DataSet) {
  3891. this.data = data;
  3892. }
  3893. else {
  3894. this.data = new DataSet({
  3895. fieldTypes: {
  3896. start: 'Date',
  3897. end: 'Date'
  3898. }
  3899. });
  3900. this.data.add(data);
  3901. }
  3902. // subscribe to new dataset
  3903. var id = this.id;
  3904. util.forEach(this.listeners, function (callback, event) {
  3905. me.data.subscribe(event, callback, id);
  3906. });
  3907. // draw all new items
  3908. dataItems = this.data.get({fields: ['id']});
  3909. ids = [];
  3910. util.forEach(dataItems, function (dataItem, index) {
  3911. ids[index] = dataItem.id;
  3912. });
  3913. this._onAdd(ids);
  3914. };
  3915. /**
  3916. * Get the data range of the item set.
  3917. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  3918. * When no minimum is found, min==null
  3919. * When no maximum is found, max==null
  3920. */
  3921. ItemSet.prototype.getDataRange = function getDataRange() {
  3922. // calculate min from start filed
  3923. var data = this.data;
  3924. var min = data.min('start');
  3925. min = min ? min.start.valueOf() : null;
  3926. // calculate max of both start and end fields
  3927. var maxStart = data.max('start');
  3928. var maxEnd = data.max('end');
  3929. maxStart = maxStart ? maxStart.start.valueOf() : null;
  3930. maxEnd = maxEnd ? maxEnd.end.valueOf() : null;
  3931. var max = Math.max(maxStart, maxEnd);
  3932. return {
  3933. min: new Date(min),
  3934. max: new Date(max)
  3935. };
  3936. };
  3937. /**
  3938. * Handle updated items
  3939. * @param {Number[]} ids
  3940. * @private
  3941. */
  3942. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  3943. this._toQueue(ids, 'update');
  3944. };
  3945. /**
  3946. * Handle changed items
  3947. * @param {Number[]} ids
  3948. * @private
  3949. */
  3950. ItemSet.prototype._onAdd = function _onAdd(ids) {
  3951. this._toQueue(ids, 'add');
  3952. };
  3953. /**
  3954. * Handle removed items
  3955. * @param {Number[]} ids
  3956. * @private
  3957. */
  3958. ItemSet.prototype._onRemove = function _onRemove(ids) {
  3959. this._toQueue(ids, 'remove');
  3960. };
  3961. /**
  3962. * Put items in the queue to be added/updated/remove
  3963. * @param {Number[]} ids
  3964. * @param {String} action can be 'add', 'update', 'remove'
  3965. */
  3966. ItemSet.prototype._toQueue = function _toQueue(ids, action) {
  3967. var items = this.items;
  3968. var queue = this.queue;
  3969. ids.forEach(function (id) {
  3970. var entry = queue[id];
  3971. if (entry) {
  3972. // already queued, update the action of the entry
  3973. entry.action = action;
  3974. }
  3975. else {
  3976. // not yet queued, add an entry to the queue
  3977. queue[id] = {
  3978. item: items[id] || null,
  3979. action: action
  3980. };
  3981. }
  3982. });
  3983. if (this.controller) {
  3984. //this.requestReflow();
  3985. this.requestRepaint();
  3986. }
  3987. };
  3988. /**
  3989. * Calculate the factor and offset to convert a position on screen to the
  3990. * corresponding date and vice versa.
  3991. * After the method _updateConversion is executed once, the methods toTime
  3992. * and toScreen can be used.
  3993. * @private
  3994. */
  3995. ItemSet.prototype._updateConversion = function _updateConversion() {
  3996. var range = this.range;
  3997. if (!range) {
  3998. throw new Error('No range configured');
  3999. }
  4000. if (range.conversion) {
  4001. this.conversion = range.conversion(this.width);
  4002. }
  4003. else {
  4004. this.conversion = Range.conversion(range.start, range.end, this.width);
  4005. }
  4006. };
  4007. /**
  4008. * Convert a position on screen (pixels) to a datetime
  4009. * Before this method can be used, the method _updateConversion must be
  4010. * executed once.
  4011. * @param {int} x Position on the screen in pixels
  4012. * @return {Date} time The datetime the corresponds with given position x
  4013. */
  4014. ItemSet.prototype.toTime = function toTime(x) {
  4015. var conversion = this.conversion;
  4016. return new Date(x / conversion.factor + conversion.offset);
  4017. };
  4018. /**
  4019. * Convert a datetime (Date object) into a position on the screen
  4020. * Before this method can be used, the method _updateConversion must be
  4021. * executed once.
  4022. * @param {Date} time A date
  4023. * @return {int} x The position on the screen in pixels which corresponds
  4024. * with the given date.
  4025. */
  4026. ItemSet.prototype.toScreen = function toScreen(time) {
  4027. var conversion = this.conversion;
  4028. return (time.valueOf() - conversion.offset) * conversion.factor;
  4029. };
  4030. /**
  4031. * @constructor Item
  4032. * @param {ItemSet} parent
  4033. * @param {Object} data Object containing (optional) parameters type,
  4034. * start, end, content, group, className.
  4035. * @param {Object} [options] Options to set initial property values
  4036. * // TODO: describe available options
  4037. */
  4038. function Item (parent, data, options) {
  4039. this.parent = parent;
  4040. this.data = data;
  4041. this.dom = null;
  4042. this.options = options;
  4043. this.selected = false;
  4044. this.visible = false;
  4045. this.top = 0;
  4046. this.left = 0;
  4047. this.width = 0;
  4048. this.height = 0;
  4049. }
  4050. /**
  4051. * Select current item
  4052. */
  4053. Item.prototype.select = function select() {
  4054. this.selected = true;
  4055. };
  4056. /**
  4057. * Unselect current item
  4058. */
  4059. Item.prototype.unselect = function unselect() {
  4060. this.selected = false;
  4061. };
  4062. /**
  4063. * Show the Item in the DOM (when not already visible)
  4064. * @return {Boolean} changed
  4065. */
  4066. Item.prototype.show = function show() {
  4067. return false;
  4068. };
  4069. /**
  4070. * Hide the Item from the DOM (when visible)
  4071. * @return {Boolean} changed
  4072. */
  4073. Item.prototype.hide = function hide() {
  4074. return false;
  4075. };
  4076. /**
  4077. * Repaint the item
  4078. * @return {Boolean} changed
  4079. */
  4080. Item.prototype.repaint = function repaint() {
  4081. // should be implemented by the item
  4082. return false;
  4083. };
  4084. /**
  4085. * Reflow the item
  4086. * @return {Boolean} resized
  4087. */
  4088. Item.prototype.reflow = function reflow() {
  4089. // should be implemented by the item
  4090. return false;
  4091. };
  4092. /**
  4093. * @constructor ItemBox
  4094. * @extends Item
  4095. * @param {ItemSet} parent
  4096. * @param {Object} data Object containing parameters start
  4097. * content, className.
  4098. * @param {Object} [options] Options to set initial property values
  4099. * // TODO: describe available options
  4100. */
  4101. function ItemBox (parent, data, options) {
  4102. this.props = {
  4103. dot: {
  4104. left: 0,
  4105. top: 0,
  4106. width: 0,
  4107. height: 0
  4108. },
  4109. line: {
  4110. top: 0,
  4111. left: 0,
  4112. width: 0,
  4113. height: 0
  4114. }
  4115. };
  4116. Item.call(this, parent, data, options);
  4117. }
  4118. ItemBox.prototype = new Item (null, null);
  4119. /**
  4120. * Select the item
  4121. * @override
  4122. */
  4123. ItemBox.prototype.select = function select() {
  4124. this.selected = true;
  4125. // TODO: select and unselect
  4126. };
  4127. /**
  4128. * Unselect the item
  4129. * @override
  4130. */
  4131. ItemBox.prototype.unselect = function unselect() {
  4132. this.selected = false;
  4133. // TODO: select and unselect
  4134. };
  4135. /**
  4136. * Repaint the item
  4137. * @return {Boolean} changed
  4138. */
  4139. ItemBox.prototype.repaint = function repaint() {
  4140. // TODO: make an efficient repaint
  4141. var changed = false;
  4142. var dom = this.dom;
  4143. if (!dom) {
  4144. this._create();
  4145. dom = this.dom;
  4146. changed = true;
  4147. }
  4148. if (dom) {
  4149. if (!this.options && !this.parent) {
  4150. throw new Error('Cannot repaint item: no parent attached');
  4151. }
  4152. var foreground = this.parent.getForeground();
  4153. if (!foreground) {
  4154. throw new Error('Cannot repaint time axis: ' +
  4155. 'parent has no foreground container element');
  4156. }
  4157. var background = this.parent.getBackground();
  4158. if (!background) {
  4159. throw new Error('Cannot repaint time axis: ' +
  4160. 'parent has no background container element');
  4161. }
  4162. var axis = this.parent.getAxis();
  4163. if (!background) {
  4164. throw new Error('Cannot repaint time axis: ' +
  4165. 'parent has no axis container element');
  4166. }
  4167. if (!dom.box.parentNode) {
  4168. foreground.appendChild(dom.box);
  4169. changed = true;
  4170. }
  4171. if (!dom.line.parentNode) {
  4172. background.appendChild(dom.line);
  4173. changed = true;
  4174. }
  4175. if (!dom.dot.parentNode) {
  4176. axis.appendChild(dom.dot);
  4177. changed = true;
  4178. }
  4179. // update contents
  4180. if (this.data.content != this.content) {
  4181. this.content = this.data.content;
  4182. if (this.content instanceof Element) {
  4183. dom.content.innerHTML = '';
  4184. dom.content.appendChild(this.content);
  4185. }
  4186. else if (this.data.content != undefined) {
  4187. dom.content.innerHTML = this.content;
  4188. }
  4189. else {
  4190. throw new Error('Property "content" missing in item ' + this.data.id);
  4191. }
  4192. changed = true;
  4193. }
  4194. // update class
  4195. var className = (this.data.className? ' ' + this.data.className : '') +
  4196. (this.selected ? ' selected' : '');
  4197. if (this.className != className) {
  4198. this.className = className;
  4199. dom.box.className = 'item box' + className;
  4200. dom.line.className = 'item line' + className;
  4201. dom.dot.className = 'item dot' + className;
  4202. changed = true;
  4203. }
  4204. }
  4205. return changed;
  4206. };
  4207. /**
  4208. * Show the item in the DOM (when not already visible). The items DOM will
  4209. * be created when needed.
  4210. * @return {Boolean} changed
  4211. */
  4212. ItemBox.prototype.show = function show() {
  4213. if (!this.dom || !this.dom.box.parentNode) {
  4214. return this.repaint();
  4215. }
  4216. else {
  4217. return false;
  4218. }
  4219. };
  4220. /**
  4221. * Hide the item from the DOM (when visible)
  4222. * @return {Boolean} changed
  4223. */
  4224. ItemBox.prototype.hide = function hide() {
  4225. var changed = false,
  4226. dom = this.dom;
  4227. if (dom) {
  4228. if (dom.box.parentNode) {
  4229. dom.box.parentNode.removeChild(dom.box);
  4230. changed = true;
  4231. }
  4232. if (dom.line.parentNode) {
  4233. dom.line.parentNode.removeChild(dom.line);
  4234. }
  4235. if (dom.dot.parentNode) {
  4236. dom.dot.parentNode.removeChild(dom.dot);
  4237. }
  4238. }
  4239. return changed;
  4240. };
  4241. /**
  4242. * Reflow the item: calculate its actual size and position from the DOM
  4243. * @return {boolean} resized returns true if the axis is resized
  4244. * @override
  4245. */
  4246. ItemBox.prototype.reflow = function reflow() {
  4247. var changed = 0,
  4248. update,
  4249. dom,
  4250. props,
  4251. options,
  4252. start,
  4253. align,
  4254. orientation,
  4255. top,
  4256. left,
  4257. data,
  4258. range;
  4259. if (this.data.start == undefined) {
  4260. throw new Error('Property "start" missing in item ' + this.data.id);
  4261. }
  4262. data = this.data;
  4263. range = this.parent && this.parent.range;
  4264. if (data && range) {
  4265. // TODO: account for the width of the item. Take some margin
  4266. this.visible = (data.start > range.start) && (data.start < range.end);
  4267. }
  4268. else {
  4269. this.visible = false;
  4270. }
  4271. if (this.visible) {
  4272. dom = this.dom;
  4273. if (dom) {
  4274. update = util.updateProperty;
  4275. props = this.props;
  4276. options = this.options;
  4277. start = this.parent.toScreen(this.data.start);
  4278. align = options && options.align;
  4279. orientation = options.orientation;
  4280. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4281. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4282. changed += update(props.line, 'width', dom.line.offsetWidth);
  4283. changed += update(props.line, 'width', dom.line.offsetWidth);
  4284. changed += update(this, 'width', dom.box.offsetWidth);
  4285. changed += update(this, 'height', dom.box.offsetHeight);
  4286. if (align == 'right') {
  4287. left = start - this.width;
  4288. }
  4289. else if (align == 'left') {
  4290. left = start;
  4291. }
  4292. else {
  4293. // default or 'center'
  4294. left = start - this.width / 2;
  4295. }
  4296. update(this, 'left', left);
  4297. update(props.line, 'left', start - props.line.width / 2);
  4298. update(props.dot, 'left', start - props.dot.width / 2);
  4299. if (orientation == 'top') {
  4300. top = options.margin.axis;
  4301. update(this, 'top', top);
  4302. update(props.line, 'top', 0);
  4303. update(props.line, 'height', top);
  4304. update(props.dot, 'top', -props.dot.height / 2);
  4305. }
  4306. else {
  4307. // default or 'bottom'
  4308. var parentHeight = this.parent.height;
  4309. top = parentHeight - this.height - options.margin.axis;
  4310. update(this, 'top', top);
  4311. update(props.line, 'top', top + this.height);
  4312. update(props.line, 'height', Math.max(options.margin.axis, 0));
  4313. update(props.dot, 'top', parentHeight - props.dot.height / 2);
  4314. }
  4315. }
  4316. else {
  4317. changed += 1;
  4318. }
  4319. }
  4320. return (changed > 0);
  4321. };
  4322. /**
  4323. * Create an items DOM
  4324. * @private
  4325. */
  4326. ItemBox.prototype._create = function _create() {
  4327. var dom = this.dom;
  4328. if (!dom) {
  4329. this.dom = dom = {};
  4330. // create the box
  4331. dom.box = document.createElement('DIV');
  4332. // className is updated in repaint()
  4333. // contents box (inside the background box). used for making margins
  4334. dom.content = document.createElement('DIV');
  4335. dom.content.className = 'content';
  4336. dom.box.appendChild(dom.content);
  4337. // line to axis
  4338. dom.line = document.createElement('DIV');
  4339. dom.line.className = 'line';
  4340. // dot on axis
  4341. dom.dot = document.createElement('DIV');
  4342. dom.dot.className = 'dot';
  4343. }
  4344. };
  4345. /**
  4346. * Reposition the item, recalculate its left, top, and width, using the current
  4347. * range and size of the items itemset
  4348. * @override
  4349. */
  4350. ItemBox.prototype.reposition = function reposition() {
  4351. var dom = this.dom,
  4352. props = this.props,
  4353. orientation = this.options.orientation;
  4354. if (dom) {
  4355. var box = dom.box,
  4356. line = dom.line,
  4357. dot = dom.dot;
  4358. box.style.left = this.left + 'px';
  4359. box.style.top = this.top + 'px';
  4360. line.style.left = props.line.left + 'px';
  4361. if (orientation == 'top') {
  4362. line.style.top = 0 + 'px';
  4363. line.style.height = this.top + 'px';
  4364. }
  4365. else {
  4366. // orientation 'bottom'
  4367. line.style.top = props.line.top + 'px';
  4368. line.style.top = (this.top + this.height) + 'px';
  4369. line.style.height = Math.max(props.dot.top - this.top - this.height, 0) + 'px';
  4370. }
  4371. dot.style.left = props.dot.left + 'px';
  4372. dot.style.top = props.dot.top + 'px';
  4373. }
  4374. };
  4375. /**
  4376. * @constructor ItemPoint
  4377. * @extends Item
  4378. * @param {ItemSet} parent
  4379. * @param {Object} data Object containing parameters start
  4380. * content, className.
  4381. * @param {Object} [options] Options to set initial property values
  4382. * // TODO: describe available options
  4383. */
  4384. function ItemPoint (parent, data, options) {
  4385. this.props = {
  4386. dot: {
  4387. top: 0,
  4388. width: 0,
  4389. height: 0
  4390. },
  4391. content: {
  4392. height: 0,
  4393. marginLeft: 0
  4394. }
  4395. };
  4396. Item.call(this, parent, data, options);
  4397. }
  4398. ItemPoint.prototype = new Item (null, null);
  4399. /**
  4400. * Select the item
  4401. * @override
  4402. */
  4403. ItemPoint.prototype.select = function select() {
  4404. this.selected = true;
  4405. // TODO: select and unselect
  4406. };
  4407. /**
  4408. * Unselect the item
  4409. * @override
  4410. */
  4411. ItemPoint.prototype.unselect = function unselect() {
  4412. this.selected = false;
  4413. // TODO: select and unselect
  4414. };
  4415. /**
  4416. * Repaint the item
  4417. * @return {Boolean} changed
  4418. */
  4419. ItemPoint.prototype.repaint = function repaint() {
  4420. // TODO: make an efficient repaint
  4421. var changed = false;
  4422. var dom = this.dom;
  4423. if (!dom) {
  4424. this._create();
  4425. dom = this.dom;
  4426. changed = true;
  4427. }
  4428. if (dom) {
  4429. if (!this.options && !this.options.parent) {
  4430. throw new Error('Cannot repaint item: no parent attached');
  4431. }
  4432. var foreground = this.parent.getForeground();
  4433. if (!foreground) {
  4434. throw new Error('Cannot repaint time axis: ' +
  4435. 'parent has no foreground container element');
  4436. }
  4437. if (!dom.point.parentNode) {
  4438. foreground.appendChild(dom.point);
  4439. foreground.appendChild(dom.point);
  4440. changed = true;
  4441. }
  4442. // update contents
  4443. if (this.data.content != this.content) {
  4444. this.content = this.data.content;
  4445. if (this.content instanceof Element) {
  4446. dom.content.innerHTML = '';
  4447. dom.content.appendChild(this.content);
  4448. }
  4449. else if (this.data.content != undefined) {
  4450. dom.content.innerHTML = this.content;
  4451. }
  4452. else {
  4453. throw new Error('Property "content" missing in item ' + this.data.id);
  4454. }
  4455. changed = true;
  4456. }
  4457. // update class
  4458. var className = (this.data.className? ' ' + this.data.className : '') +
  4459. (this.selected ? ' selected' : '');
  4460. if (this.className != className) {
  4461. this.className = className;
  4462. dom.point.className = 'item point' + className;
  4463. changed = true;
  4464. }
  4465. }
  4466. return changed;
  4467. };
  4468. /**
  4469. * Show the item in the DOM (when not already visible). The items DOM will
  4470. * be created when needed.
  4471. * @return {Boolean} changed
  4472. */
  4473. ItemPoint.prototype.show = function show() {
  4474. if (!this.dom || !this.dom.point.parentNode) {
  4475. return this.repaint();
  4476. }
  4477. else {
  4478. return false;
  4479. }
  4480. };
  4481. /**
  4482. * Hide the item from the DOM (when visible)
  4483. * @return {Boolean} changed
  4484. */
  4485. ItemPoint.prototype.hide = function hide() {
  4486. var changed = false,
  4487. dom = this.dom;
  4488. if (dom) {
  4489. if (dom.point.parentNode) {
  4490. dom.point.parentNode.removeChild(dom.point);
  4491. changed = true;
  4492. }
  4493. }
  4494. return changed;
  4495. };
  4496. /**
  4497. * Reflow the item: calculate its actual size from the DOM
  4498. * @return {boolean} resized returns true if the axis is resized
  4499. * @override
  4500. */
  4501. ItemPoint.prototype.reflow = function reflow() {
  4502. var changed = 0,
  4503. update,
  4504. dom,
  4505. props,
  4506. options,
  4507. orientation,
  4508. start,
  4509. top,
  4510. data,
  4511. range;
  4512. if (this.data.start == undefined) {
  4513. throw new Error('Property "start" missing in item ' + this.data.id);
  4514. }
  4515. data = this.data;
  4516. range = this.parent && this.parent.range;
  4517. if (data && range) {
  4518. // TODO: account for the width of the item. Take some margin
  4519. this.visible = (data.start > range.start) && (data.start < range.end);
  4520. }
  4521. else {
  4522. this.visible = false;
  4523. }
  4524. if (this.visible) {
  4525. dom = this.dom;
  4526. if (dom) {
  4527. update = util.updateProperty;
  4528. props = this.props;
  4529. options = this.options;
  4530. orientation = options.orientation;
  4531. start = this.parent.toScreen(this.data.start);
  4532. changed += update(this, 'width', dom.point.offsetWidth);
  4533. changed += update(this, 'height', dom.point.offsetHeight);
  4534. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4535. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4536. changed += update(props.content, 'height', dom.content.offsetHeight);
  4537. if (orientation == 'top') {
  4538. top = options.margin.axis;
  4539. }
  4540. else {
  4541. // default or 'bottom'
  4542. var parentHeight = this.parent.height;
  4543. top = Math.max(parentHeight - this.height - options.margin.axis, 0);
  4544. }
  4545. changed += update(this, 'top', top);
  4546. changed += update(this, 'left', start - props.dot.width / 2);
  4547. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  4548. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  4549. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  4550. }
  4551. else {
  4552. changed += 1;
  4553. }
  4554. }
  4555. return (changed > 0);
  4556. };
  4557. /**
  4558. * Create an items DOM
  4559. * @private
  4560. */
  4561. ItemPoint.prototype._create = function _create() {
  4562. var dom = this.dom;
  4563. if (!dom) {
  4564. this.dom = dom = {};
  4565. // background box
  4566. dom.point = document.createElement('div');
  4567. // className is updated in repaint()
  4568. // contents box, right from the dot
  4569. dom.content = document.createElement('div');
  4570. dom.content.className = 'content';
  4571. dom.point.appendChild(dom.content);
  4572. // dot at start
  4573. dom.dot = document.createElement('div');
  4574. dom.dot.className = 'dot';
  4575. dom.point.appendChild(dom.dot);
  4576. }
  4577. };
  4578. /**
  4579. * Reposition the item, recalculate its left, top, and width, using the current
  4580. * range and size of the items itemset
  4581. * @override
  4582. */
  4583. ItemPoint.prototype.reposition = function reposition() {
  4584. var dom = this.dom,
  4585. props = this.props;
  4586. if (dom) {
  4587. dom.point.style.top = this.top + 'px';
  4588. dom.point.style.left = this.left + 'px';
  4589. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  4590. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  4591. dom.dot.style.top = props.dot.top + 'px';
  4592. }
  4593. };
  4594. /**
  4595. * @constructor ItemRange
  4596. * @extends Item
  4597. * @param {ItemSet} parent
  4598. * @param {Object} data Object containing parameters start, end
  4599. * content, className.
  4600. * @param {Object} [options] Options to set initial property values
  4601. * // TODO: describe available options
  4602. */
  4603. function ItemRange (parent, data, options) {
  4604. this.props = {
  4605. content: {
  4606. left: 0,
  4607. width: 0
  4608. }
  4609. };
  4610. Item.call(this, parent, data, options);
  4611. }
  4612. ItemRange.prototype = new Item (null, null);
  4613. /**
  4614. * Select the item
  4615. * @override
  4616. */
  4617. ItemRange.prototype.select = function select() {
  4618. this.selected = true;
  4619. // TODO: select and unselect
  4620. };
  4621. /**
  4622. * Unselect the item
  4623. * @override
  4624. */
  4625. ItemRange.prototype.unselect = function unselect() {
  4626. this.selected = false;
  4627. // TODO: select and unselect
  4628. };
  4629. /**
  4630. * Repaint the item
  4631. * @return {Boolean} changed
  4632. */
  4633. ItemRange.prototype.repaint = function repaint() {
  4634. // TODO: make an efficient repaint
  4635. var changed = false;
  4636. var dom = this.dom;
  4637. if (!dom) {
  4638. this._create();
  4639. dom = this.dom;
  4640. changed = true;
  4641. }
  4642. if (dom) {
  4643. if (!this.options && !this.options.parent) {
  4644. throw new Error('Cannot repaint item: no parent attached');
  4645. }
  4646. var foreground = this.parent.getForeground();
  4647. if (!foreground) {
  4648. throw new Error('Cannot repaint time axis: ' +
  4649. 'parent has no foreground container element');
  4650. }
  4651. if (!dom.box.parentNode) {
  4652. foreground.appendChild(dom.box);
  4653. changed = true;
  4654. }
  4655. // update content
  4656. if (this.data.content != this.content) {
  4657. this.content = this.data.content;
  4658. if (this.content instanceof Element) {
  4659. dom.content.innerHTML = '';
  4660. dom.content.appendChild(this.content);
  4661. }
  4662. else if (this.data.content != undefined) {
  4663. dom.content.innerHTML = this.content;
  4664. }
  4665. else {
  4666. throw new Error('Property "content" missing in item ' + this.data.id);
  4667. }
  4668. changed = true;
  4669. }
  4670. // update class
  4671. var className = this.data.className ? ('' + this.data.className) : '';
  4672. if (this.className != className) {
  4673. this.className = className;
  4674. dom.box.className = 'item range' + className;
  4675. changed = true;
  4676. }
  4677. }
  4678. return changed;
  4679. };
  4680. /**
  4681. * Show the item in the DOM (when not already visible). The items DOM will
  4682. * be created when needed.
  4683. * @return {Boolean} changed
  4684. */
  4685. ItemRange.prototype.show = function show() {
  4686. if (!this.dom || !this.dom.box.parentNode) {
  4687. return this.repaint();
  4688. }
  4689. else {
  4690. return false;
  4691. }
  4692. };
  4693. /**
  4694. * Hide the item from the DOM (when visible)
  4695. * @return {Boolean} changed
  4696. */
  4697. ItemRange.prototype.hide = function hide() {
  4698. var changed = false,
  4699. dom = this.dom;
  4700. if (dom) {
  4701. if (dom.box.parentNode) {
  4702. dom.box.parentNode.removeChild(dom.box);
  4703. changed = true;
  4704. }
  4705. }
  4706. return changed;
  4707. };
  4708. /**
  4709. * Reflow the item: calculate its actual size from the DOM
  4710. * @return {boolean} resized returns true if the axis is resized
  4711. * @override
  4712. */
  4713. ItemRange.prototype.reflow = function reflow() {
  4714. var changed = 0,
  4715. dom,
  4716. props,
  4717. options,
  4718. parent,
  4719. start,
  4720. end,
  4721. data,
  4722. range,
  4723. update,
  4724. box,
  4725. parentWidth,
  4726. contentLeft,
  4727. orientation,
  4728. top;
  4729. if (this.data.start == undefined) {
  4730. throw new Error('Property "start" missing in item ' + this.data.id);
  4731. }
  4732. if (this.data.end == undefined) {
  4733. throw new Error('Property "end" missing in item ' + this.data.id);
  4734. }
  4735. data = this.data;
  4736. range = this.parent && this.parent.range;
  4737. if (data && range) {
  4738. // TODO: account for the width of the item. Take some margin
  4739. this.visible = (data.start < range.end) && (data.end > range.start);
  4740. }
  4741. else {
  4742. this.visible = false;
  4743. }
  4744. if (this.visible) {
  4745. dom = this.dom;
  4746. if (dom) {
  4747. props = this.props;
  4748. options = this.options;
  4749. parent = this.parent;
  4750. start = parent.toScreen(this.data.start);
  4751. end = parent.toScreen(this.data.end);
  4752. update = util.updateProperty;
  4753. box = dom.box;
  4754. parentWidth = parent.width;
  4755. orientation = options.orientation;
  4756. changed += update(props.content, 'width', dom.content.offsetWidth);
  4757. changed += update(this, 'height', box.offsetHeight);
  4758. // limit the width of the this, as browsers cannot draw very wide divs
  4759. if (start < -parentWidth) {
  4760. start = -parentWidth;
  4761. }
  4762. if (end > 2 * parentWidth) {
  4763. end = 2 * parentWidth;
  4764. }
  4765. // when range exceeds left of the window, position the contents at the left of the visible area
  4766. if (start < 0) {
  4767. contentLeft = Math.min(-start,
  4768. (end - start - props.content.width - 2 * options.padding));
  4769. // TODO: remove the need for options.padding. it's terrible.
  4770. }
  4771. else {
  4772. contentLeft = 0;
  4773. }
  4774. changed += update(props.content, 'left', contentLeft);
  4775. if (orientation == 'top') {
  4776. top = options.margin.axis;
  4777. changed += update(this, 'top', top);
  4778. }
  4779. else {
  4780. // default or 'bottom'
  4781. top = parent.height - this.height - options.margin.axis;
  4782. changed += update(this, 'top', top);
  4783. }
  4784. changed += update(this, 'left', start);
  4785. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  4786. }
  4787. else {
  4788. changed += 1;
  4789. }
  4790. }
  4791. return (changed > 0);
  4792. };
  4793. /**
  4794. * Create an items DOM
  4795. * @private
  4796. */
  4797. ItemRange.prototype._create = function _create() {
  4798. var dom = this.dom;
  4799. if (!dom) {
  4800. this.dom = dom = {};
  4801. // background box
  4802. dom.box = document.createElement('div');
  4803. // className is updated in repaint()
  4804. // contents box
  4805. dom.content = document.createElement('div');
  4806. dom.content.className = 'content';
  4807. dom.box.appendChild(dom.content);
  4808. }
  4809. };
  4810. /**
  4811. * Reposition the item, recalculate its left, top, and width, using the current
  4812. * range and size of the items itemset
  4813. * @override
  4814. */
  4815. ItemRange.prototype.reposition = function reposition() {
  4816. var dom = this.dom,
  4817. props = this.props;
  4818. if (dom) {
  4819. dom.box.style.top = this.top + 'px';
  4820. dom.box.style.left = this.left + 'px';
  4821. dom.box.style.width = this.width + 'px';
  4822. dom.content.style.left = props.content.left + 'px';
  4823. }
  4824. };
  4825. /**
  4826. * Create a timeline visualization
  4827. * @param {HTMLElement} container
  4828. * @param {DataSet | Array | DataTable} [data]
  4829. * @param {Object} [options] See Timeline.setOptions for the available options.
  4830. * @constructor
  4831. */
  4832. function Timeline (container, data, options) {
  4833. var me = this;
  4834. this.options = {
  4835. orientation: 'bottom',
  4836. zoomMin: 10, // milliseconds
  4837. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  4838. moveable: true,
  4839. zoomable: true
  4840. };
  4841. // controller
  4842. this.controller = new Controller();
  4843. // main panel
  4844. if (!container) {
  4845. throw new Error('No container element provided');
  4846. }
  4847. this.main = new RootPanel(container, {
  4848. autoResize: false
  4849. });
  4850. this.controller.add(this.main);
  4851. // range
  4852. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  4853. this.range = new Range({
  4854. start: now.clone().add('days', -3).valueOf(),
  4855. end: now.clone().add('days', 4).valueOf()
  4856. });
  4857. // TODO: reckon with options moveable and zoomable
  4858. this.range.subscribe(this.main, 'move', 'horizontal');
  4859. this.range.subscribe(this.main, 'zoom', 'horizontal');
  4860. this.range.on('rangechange', function () {
  4861. var force = true;
  4862. me.controller.requestReflow(force);
  4863. });
  4864. this.range.on('rangechanged', function () {
  4865. var force = true;
  4866. me.controller.requestReflow(force);
  4867. });
  4868. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  4869. // time axis
  4870. this.timeaxis = new TimeAxis(this.main, [], {
  4871. orientation: this.options.orientation,
  4872. range: this.range
  4873. });
  4874. this.timeaxis.setRange(this.range);
  4875. this.controller.add(this.timeaxis);
  4876. // items panel
  4877. this.itemset = new ItemSet(this.main, [this.timeaxis], {
  4878. orientation: this.options.orientation
  4879. });
  4880. this.itemset.setRange(this.range);
  4881. this.controller.add(this.itemset);
  4882. // set options (must take place before setting the data)
  4883. this.setOptions(options);
  4884. // set data
  4885. if (data) {
  4886. this.setData(data);
  4887. }
  4888. }
  4889. /**
  4890. * Set options
  4891. * @param {Object} options TODO: describe the available options
  4892. */
  4893. Timeline.prototype.setOptions = function (options) {
  4894. util.extend(this.options, options);
  4895. // update options the timeaxis
  4896. this.timeaxis.setOptions(this.options);
  4897. // update options for the range
  4898. this.range.setOptions(this.options);
  4899. // update options the itemset
  4900. var itemsTop,
  4901. itemsHeight,
  4902. mainHeight,
  4903. maxHeight,
  4904. me = this;
  4905. if (this.options.orientation == 'top') {
  4906. itemsTop = function () {
  4907. return me.timeaxis.height;
  4908. }
  4909. }
  4910. else {
  4911. itemsTop = function () {
  4912. return me.main.height - me.timeaxis.height - me.itemset.height;
  4913. }
  4914. }
  4915. if (options.height) {
  4916. // fixed height
  4917. mainHeight = options.height;
  4918. itemsHeight = function () {
  4919. return me.main.height - me.timeaxis.height;
  4920. };
  4921. }
  4922. else {
  4923. // auto height
  4924. mainHeight = function () {
  4925. return me.timeaxis.height + me.itemset.height;
  4926. };
  4927. itemsHeight = null;
  4928. }
  4929. // TODO: maxHeight should be a string in px or % (currently only accepts a number)
  4930. if (this.options.maxHeight) {
  4931. if (!util.isNumber(this.options.maxHeight)) {
  4932. throw new TypeError('Number expected for property maxHeight');
  4933. }
  4934. maxHeight = function () {
  4935. return me.options.maxHeight - me.timeaxis.height;
  4936. }
  4937. }
  4938. this.main.setOptions({
  4939. height: mainHeight
  4940. });
  4941. this.itemset.setOptions({
  4942. orientation: this.options.orientation,
  4943. top: itemsTop,
  4944. height: itemsHeight,
  4945. maxHeight: maxHeight
  4946. });
  4947. this.controller.repaint();
  4948. };
  4949. /**
  4950. * Set data
  4951. * @param {DataSet | Array | DataTable} data
  4952. */
  4953. Timeline.prototype.setData = function(data) {
  4954. var dataset = this.itemset.data;
  4955. if (!dataset) {
  4956. // first load of data
  4957. this.itemset.setData(data);
  4958. if (this.options.start == undefined || this.options.end == undefined) {
  4959. // apply the data range as range
  4960. var dataRange = this.itemset.getDataRange();
  4961. // add 5% on both sides
  4962. var min = dataRange.min;
  4963. var max = dataRange.max;
  4964. if (min != null && max != null) {
  4965. var interval = (max.valueOf() - min.valueOf());
  4966. min = new Date(min.valueOf() - interval * 0.05);
  4967. max = new Date(max.valueOf() + interval * 0.05);
  4968. }
  4969. // override specified start and/or end date
  4970. if (this.options.start != undefined) {
  4971. min = new Date(this.options.start.valueOf());
  4972. }
  4973. if (this.options.end != undefined) {
  4974. max = new Date(this.options.end.valueOf());
  4975. }
  4976. // apply range if there is a min or max available
  4977. if (min != null || max != null) {
  4978. this.range.setRange(min, max);
  4979. }
  4980. }
  4981. }
  4982. else {
  4983. // updated data
  4984. this.itemset.setData(data);
  4985. }
  4986. };
  4987. /**
  4988. * vis.js module exports
  4989. */
  4990. var vis = {
  4991. util: util,
  4992. events: events,
  4993. Controller: Controller,
  4994. DataSet: DataSet,
  4995. Range: Range,
  4996. Stack: Stack,
  4997. TimeStep: TimeStep,
  4998. EventBus: EventBus,
  4999. components: {
  5000. items: {
  5001. Item: Item,
  5002. ItemBox: ItemBox,
  5003. ItemPoint: ItemPoint,
  5004. ItemRange: ItemRange
  5005. },
  5006. Component: Component,
  5007. Panel: Panel,
  5008. RootPanel: RootPanel,
  5009. ItemSet: ItemSet,
  5010. TimeAxis: TimeAxis
  5011. },
  5012. Timeline: Timeline
  5013. };
  5014. /**
  5015. * CommonJS module exports
  5016. */
  5017. if (typeof exports !== 'undefined') {
  5018. exports = vis;
  5019. }
  5020. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  5021. module.exports = vis;
  5022. }
  5023. /**
  5024. * AMD module exports
  5025. */
  5026. if (typeof(define) === 'function') {
  5027. define(function () {
  5028. return vis;
  5029. });
  5030. }
  5031. /**
  5032. * Window exports
  5033. */
  5034. if (typeof window !== 'undefined') {
  5035. // attach the module to the window, load as a regular javascript file
  5036. window['vis'] = vis;
  5037. }
  5038. // inject css
  5039. util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  5040. },{"moment":2}],2:[function(require,module,exports){
  5041. (function(){// moment.js
  5042. // version : 2.0.0
  5043. // author : Tim Wood
  5044. // license : MIT
  5045. // momentjs.com
  5046. (function (undefined) {
  5047. /************************************
  5048. Constants
  5049. ************************************/
  5050. var moment,
  5051. VERSION = "2.0.0",
  5052. round = Math.round, i,
  5053. // internal storage for language config files
  5054. languages = {},
  5055. // check for nodeJS
  5056. hasModule = (typeof module !== 'undefined' && module.exports),
  5057. // ASP.NET json date format regex
  5058. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  5059. // format tokens
  5060. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
  5061. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  5062. // parsing tokens
  5063. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  5064. // parsing token regexes
  5065. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  5066. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  5067. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  5068. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  5069. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  5070. parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
  5071. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  5072. parseTokenT = /T/i, // T (ISO seperator)
  5073. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  5074. // preliminary iso regex
  5075. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  5076. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  5077. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  5078. // iso time formats and regexes
  5079. isoTimes = [
  5080. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  5081. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  5082. ['HH:mm', /(T| )\d\d:\d\d/],
  5083. ['HH', /(T| )\d\d/]
  5084. ],
  5085. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  5086. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  5087. // getter and setter names
  5088. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  5089. unitMillisecondFactors = {
  5090. 'Milliseconds' : 1,
  5091. 'Seconds' : 1e3,
  5092. 'Minutes' : 6e4,
  5093. 'Hours' : 36e5,
  5094. 'Days' : 864e5,
  5095. 'Months' : 2592e6,
  5096. 'Years' : 31536e6
  5097. },
  5098. // format function strings
  5099. formatFunctions = {},
  5100. // tokens to ordinalize and pad
  5101. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  5102. paddedTokens = 'M D H h m s w W'.split(' '),
  5103. formatTokenFunctions = {
  5104. M : function () {
  5105. return this.month() + 1;
  5106. },
  5107. MMM : function (format) {
  5108. return this.lang().monthsShort(this, format);
  5109. },
  5110. MMMM : function (format) {
  5111. return this.lang().months(this, format);
  5112. },
  5113. D : function () {
  5114. return this.date();
  5115. },
  5116. DDD : function () {
  5117. return this.dayOfYear();
  5118. },
  5119. d : function () {
  5120. return this.day();
  5121. },
  5122. dd : function (format) {
  5123. return this.lang().weekdaysMin(this, format);
  5124. },
  5125. ddd : function (format) {
  5126. return this.lang().weekdaysShort(this, format);
  5127. },
  5128. dddd : function (format) {
  5129. return this.lang().weekdays(this, format);
  5130. },
  5131. w : function () {
  5132. return this.week();
  5133. },
  5134. W : function () {
  5135. return this.isoWeek();
  5136. },
  5137. YY : function () {
  5138. return leftZeroFill(this.year() % 100, 2);
  5139. },
  5140. YYYY : function () {
  5141. return leftZeroFill(this.year(), 4);
  5142. },
  5143. YYYYY : function () {
  5144. return leftZeroFill(this.year(), 5);
  5145. },
  5146. a : function () {
  5147. return this.lang().meridiem(this.hours(), this.minutes(), true);
  5148. },
  5149. A : function () {
  5150. return this.lang().meridiem(this.hours(), this.minutes(), false);
  5151. },
  5152. H : function () {
  5153. return this.hours();
  5154. },
  5155. h : function () {
  5156. return this.hours() % 12 || 12;
  5157. },
  5158. m : function () {
  5159. return this.minutes();
  5160. },
  5161. s : function () {
  5162. return this.seconds();
  5163. },
  5164. S : function () {
  5165. return ~~(this.milliseconds() / 100);
  5166. },
  5167. SS : function () {
  5168. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  5169. },
  5170. SSS : function () {
  5171. return leftZeroFill(this.milliseconds(), 3);
  5172. },
  5173. Z : function () {
  5174. var a = -this.zone(),
  5175. b = "+";
  5176. if (a < 0) {
  5177. a = -a;
  5178. b = "-";
  5179. }
  5180. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  5181. },
  5182. ZZ : function () {
  5183. var a = -this.zone(),
  5184. b = "+";
  5185. if (a < 0) {
  5186. a = -a;
  5187. b = "-";
  5188. }
  5189. return b + leftZeroFill(~~(10 * a / 6), 4);
  5190. },
  5191. X : function () {
  5192. return this.unix();
  5193. }
  5194. };
  5195. function padToken(func, count) {
  5196. return function (a) {
  5197. return leftZeroFill(func.call(this, a), count);
  5198. };
  5199. }
  5200. function ordinalizeToken(func) {
  5201. return function (a) {
  5202. return this.lang().ordinal(func.call(this, a));
  5203. };
  5204. }
  5205. while (ordinalizeTokens.length) {
  5206. i = ordinalizeTokens.pop();
  5207. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  5208. }
  5209. while (paddedTokens.length) {
  5210. i = paddedTokens.pop();
  5211. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  5212. }
  5213. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  5214. /************************************
  5215. Constructors
  5216. ************************************/
  5217. function Language() {
  5218. }
  5219. // Moment prototype object
  5220. function Moment(config) {
  5221. extend(this, config);
  5222. }
  5223. // Duration Constructor
  5224. function Duration(duration) {
  5225. var data = this._data = {},
  5226. years = duration.years || duration.year || duration.y || 0,
  5227. months = duration.months || duration.month || duration.M || 0,
  5228. weeks = duration.weeks || duration.week || duration.w || 0,
  5229. days = duration.days || duration.day || duration.d || 0,
  5230. hours = duration.hours || duration.hour || duration.h || 0,
  5231. minutes = duration.minutes || duration.minute || duration.m || 0,
  5232. seconds = duration.seconds || duration.second || duration.s || 0,
  5233. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  5234. // representation for dateAddRemove
  5235. this._milliseconds = milliseconds +
  5236. seconds * 1e3 + // 1000
  5237. minutes * 6e4 + // 1000 * 60
  5238. hours * 36e5; // 1000 * 60 * 60
  5239. // Because of dateAddRemove treats 24 hours as different from a
  5240. // day when working around DST, we need to store them separately
  5241. this._days = days +
  5242. weeks * 7;
  5243. // It is impossible translate months into days without knowing
  5244. // which months you are are talking about, so we have to store
  5245. // it separately.
  5246. this._months = months +
  5247. years * 12;
  5248. // The following code bubbles up values, see the tests for
  5249. // examples of what that means.
  5250. data.milliseconds = milliseconds % 1000;
  5251. seconds += absRound(milliseconds / 1000);
  5252. data.seconds = seconds % 60;
  5253. minutes += absRound(seconds / 60);
  5254. data.minutes = minutes % 60;
  5255. hours += absRound(minutes / 60);
  5256. data.hours = hours % 24;
  5257. days += absRound(hours / 24);
  5258. days += weeks * 7;
  5259. data.days = days % 30;
  5260. months += absRound(days / 30);
  5261. data.months = months % 12;
  5262. years += absRound(months / 12);
  5263. data.years = years;
  5264. }
  5265. /************************************
  5266. Helpers
  5267. ************************************/
  5268. function extend(a, b) {
  5269. for (var i in b) {
  5270. if (b.hasOwnProperty(i)) {
  5271. a[i] = b[i];
  5272. }
  5273. }
  5274. return a;
  5275. }
  5276. function absRound(number) {
  5277. if (number < 0) {
  5278. return Math.ceil(number);
  5279. } else {
  5280. return Math.floor(number);
  5281. }
  5282. }
  5283. // left zero fill a number
  5284. // see http://jsperf.com/left-zero-filling for performance comparison
  5285. function leftZeroFill(number, targetLength) {
  5286. var output = number + '';
  5287. while (output.length < targetLength) {
  5288. output = '0' + output;
  5289. }
  5290. return output;
  5291. }
  5292. // helper function for _.addTime and _.subtractTime
  5293. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  5294. var ms = duration._milliseconds,
  5295. d = duration._days,
  5296. M = duration._months,
  5297. currentDate;
  5298. if (ms) {
  5299. mom._d.setTime(+mom + ms * isAdding);
  5300. }
  5301. if (d) {
  5302. mom.date(mom.date() + d * isAdding);
  5303. }
  5304. if (M) {
  5305. currentDate = mom.date();
  5306. mom.date(1)
  5307. .month(mom.month() + M * isAdding)
  5308. .date(Math.min(currentDate, mom.daysInMonth()));
  5309. }
  5310. }
  5311. // check if is an array
  5312. function isArray(input) {
  5313. return Object.prototype.toString.call(input) === '[object Array]';
  5314. }
  5315. // compare two arrays, return the number of differences
  5316. function compareArrays(array1, array2) {
  5317. var len = Math.min(array1.length, array2.length),
  5318. lengthDiff = Math.abs(array1.length - array2.length),
  5319. diffs = 0,
  5320. i;
  5321. for (i = 0; i < len; i++) {
  5322. if (~~array1[i] !== ~~array2[i]) {
  5323. diffs++;
  5324. }
  5325. }
  5326. return diffs + lengthDiff;
  5327. }
  5328. /************************************
  5329. Languages
  5330. ************************************/
  5331. Language.prototype = {
  5332. set : function (config) {
  5333. var prop, i;
  5334. for (i in config) {
  5335. prop = config[i];
  5336. if (typeof prop === 'function') {
  5337. this[i] = prop;
  5338. } else {
  5339. this['_' + i] = prop;
  5340. }
  5341. }
  5342. },
  5343. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  5344. months : function (m) {
  5345. return this._months[m.month()];
  5346. },
  5347. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  5348. monthsShort : function (m) {
  5349. return this._monthsShort[m.month()];
  5350. },
  5351. monthsParse : function (monthName) {
  5352. var i, mom, regex, output;
  5353. if (!this._monthsParse) {
  5354. this._monthsParse = [];
  5355. }
  5356. for (i = 0; i < 12; i++) {
  5357. // make the regex if we don't have it already
  5358. if (!this._monthsParse[i]) {
  5359. mom = moment([2000, i]);
  5360. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  5361. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  5362. }
  5363. // test the regex
  5364. if (this._monthsParse[i].test(monthName)) {
  5365. return i;
  5366. }
  5367. }
  5368. },
  5369. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  5370. weekdays : function (m) {
  5371. return this._weekdays[m.day()];
  5372. },
  5373. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  5374. weekdaysShort : function (m) {
  5375. return this._weekdaysShort[m.day()];
  5376. },
  5377. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  5378. weekdaysMin : function (m) {
  5379. return this._weekdaysMin[m.day()];
  5380. },
  5381. _longDateFormat : {
  5382. LT : "h:mm A",
  5383. L : "MM/DD/YYYY",
  5384. LL : "MMMM D YYYY",
  5385. LLL : "MMMM D YYYY LT",
  5386. LLLL : "dddd, MMMM D YYYY LT"
  5387. },
  5388. longDateFormat : function (key) {
  5389. var output = this._longDateFormat[key];
  5390. if (!output && this._longDateFormat[key.toUpperCase()]) {
  5391. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  5392. return val.slice(1);
  5393. });
  5394. this._longDateFormat[key] = output;
  5395. }
  5396. return output;
  5397. },
  5398. meridiem : function (hours, minutes, isLower) {
  5399. if (hours > 11) {
  5400. return isLower ? 'pm' : 'PM';
  5401. } else {
  5402. return isLower ? 'am' : 'AM';
  5403. }
  5404. },
  5405. _calendar : {
  5406. sameDay : '[Today at] LT',
  5407. nextDay : '[Tomorrow at] LT',
  5408. nextWeek : 'dddd [at] LT',
  5409. lastDay : '[Yesterday at] LT',
  5410. lastWeek : '[last] dddd [at] LT',
  5411. sameElse : 'L'
  5412. },
  5413. calendar : function (key, mom) {
  5414. var output = this._calendar[key];
  5415. return typeof output === 'function' ? output.apply(mom) : output;
  5416. },
  5417. _relativeTime : {
  5418. future : "in %s",
  5419. past : "%s ago",
  5420. s : "a few seconds",
  5421. m : "a minute",
  5422. mm : "%d minutes",
  5423. h : "an hour",
  5424. hh : "%d hours",
  5425. d : "a day",
  5426. dd : "%d days",
  5427. M : "a month",
  5428. MM : "%d months",
  5429. y : "a year",
  5430. yy : "%d years"
  5431. },
  5432. relativeTime : function (number, withoutSuffix, string, isFuture) {
  5433. var output = this._relativeTime[string];
  5434. return (typeof output === 'function') ?
  5435. output(number, withoutSuffix, string, isFuture) :
  5436. output.replace(/%d/i, number);
  5437. },
  5438. pastFuture : function (diff, output) {
  5439. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  5440. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  5441. },
  5442. ordinal : function (number) {
  5443. return this._ordinal.replace("%d", number);
  5444. },
  5445. _ordinal : "%d",
  5446. preparse : function (string) {
  5447. return string;
  5448. },
  5449. postformat : function (string) {
  5450. return string;
  5451. },
  5452. week : function (mom) {
  5453. return weekOfYear(mom, this._week.dow, this._week.doy);
  5454. },
  5455. _week : {
  5456. dow : 0, // Sunday is the first day of the week.
  5457. doy : 6 // The week that contains Jan 1st is the first week of the year.
  5458. }
  5459. };
  5460. // Loads a language definition into the `languages` cache. The function
  5461. // takes a key and optionally values. If not in the browser and no values
  5462. // are provided, it will load the language file module. As a convenience,
  5463. // this function also returns the language values.
  5464. function loadLang(key, values) {
  5465. values.abbr = key;
  5466. if (!languages[key]) {
  5467. languages[key] = new Language();
  5468. }
  5469. languages[key].set(values);
  5470. return languages[key];
  5471. }
  5472. // Determines which language definition to use and returns it.
  5473. //
  5474. // With no parameters, it will return the global language. If you
  5475. // pass in a language key, such as 'en', it will return the
  5476. // definition for 'en', so long as 'en' has already been loaded using
  5477. // moment.lang.
  5478. function getLangDefinition(key) {
  5479. if (!key) {
  5480. return moment.fn._lang;
  5481. }
  5482. if (!languages[key] && hasModule) {
  5483. require('./lang/' + key);
  5484. }
  5485. return languages[key];
  5486. }
  5487. /************************************
  5488. Formatting
  5489. ************************************/
  5490. function removeFormattingTokens(input) {
  5491. if (input.match(/\[.*\]/)) {
  5492. return input.replace(/^\[|\]$/g, "");
  5493. }
  5494. return input.replace(/\\/g, "");
  5495. }
  5496. function makeFormatFunction(format) {
  5497. var array = format.match(formattingTokens), i, length;
  5498. for (i = 0, length = array.length; i < length; i++) {
  5499. if (formatTokenFunctions[array[i]]) {
  5500. array[i] = formatTokenFunctions[array[i]];
  5501. } else {
  5502. array[i] = removeFormattingTokens(array[i]);
  5503. }
  5504. }
  5505. return function (mom) {
  5506. var output = "";
  5507. for (i = 0; i < length; i++) {
  5508. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  5509. }
  5510. return output;
  5511. };
  5512. }
  5513. // format date using native date object
  5514. function formatMoment(m, format) {
  5515. var i = 5;
  5516. function replaceLongDateFormatTokens(input) {
  5517. return m.lang().longDateFormat(input) || input;
  5518. }
  5519. while (i-- && localFormattingTokens.test(format)) {
  5520. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  5521. }
  5522. if (!formatFunctions[format]) {
  5523. formatFunctions[format] = makeFormatFunction(format);
  5524. }
  5525. return formatFunctions[format](m);
  5526. }
  5527. /************************************
  5528. Parsing
  5529. ************************************/
  5530. // get the regex to find the next token
  5531. function getParseRegexForToken(token) {
  5532. switch (token) {
  5533. case 'DDDD':
  5534. return parseTokenThreeDigits;
  5535. case 'YYYY':
  5536. return parseTokenFourDigits;
  5537. case 'YYYYY':
  5538. return parseTokenSixDigits;
  5539. case 'S':
  5540. case 'SS':
  5541. case 'SSS':
  5542. case 'DDD':
  5543. return parseTokenOneToThreeDigits;
  5544. case 'MMM':
  5545. case 'MMMM':
  5546. case 'dd':
  5547. case 'ddd':
  5548. case 'dddd':
  5549. case 'a':
  5550. case 'A':
  5551. return parseTokenWord;
  5552. case 'X':
  5553. return parseTokenTimestampMs;
  5554. case 'Z':
  5555. case 'ZZ':
  5556. return parseTokenTimezone;
  5557. case 'T':
  5558. return parseTokenT;
  5559. case 'MM':
  5560. case 'DD':
  5561. case 'YY':
  5562. case 'HH':
  5563. case 'hh':
  5564. case 'mm':
  5565. case 'ss':
  5566. case 'M':
  5567. case 'D':
  5568. case 'd':
  5569. case 'H':
  5570. case 'h':
  5571. case 'm':
  5572. case 's':
  5573. return parseTokenOneOrTwoDigits;
  5574. default :
  5575. return new RegExp(token.replace('\\', ''));
  5576. }
  5577. }
  5578. // function to convert string input to date
  5579. function addTimeToArrayFromToken(token, input, config) {
  5580. var a, b,
  5581. datePartArray = config._a;
  5582. switch (token) {
  5583. // MONTH
  5584. case 'M' : // fall through to MM
  5585. case 'MM' :
  5586. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  5587. break;
  5588. case 'MMM' : // fall through to MMMM
  5589. case 'MMMM' :
  5590. a = getLangDefinition(config._l).monthsParse(input);
  5591. // if we didn't find a month name, mark the date as invalid.
  5592. if (a != null) {
  5593. datePartArray[1] = a;
  5594. } else {
  5595. config._isValid = false;
  5596. }
  5597. break;
  5598. // DAY OF MONTH
  5599. case 'D' : // fall through to DDDD
  5600. case 'DD' : // fall through to DDDD
  5601. case 'DDD' : // fall through to DDDD
  5602. case 'DDDD' :
  5603. if (input != null) {
  5604. datePartArray[2] = ~~input;
  5605. }
  5606. break;
  5607. // YEAR
  5608. case 'YY' :
  5609. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  5610. break;
  5611. case 'YYYY' :
  5612. case 'YYYYY' :
  5613. datePartArray[0] = ~~input;
  5614. break;
  5615. // AM / PM
  5616. case 'a' : // fall through to A
  5617. case 'A' :
  5618. config._isPm = ((input + '').toLowerCase() === 'pm');
  5619. break;
  5620. // 24 HOUR
  5621. case 'H' : // fall through to hh
  5622. case 'HH' : // fall through to hh
  5623. case 'h' : // fall through to hh
  5624. case 'hh' :
  5625. datePartArray[3] = ~~input;
  5626. break;
  5627. // MINUTE
  5628. case 'm' : // fall through to mm
  5629. case 'mm' :
  5630. datePartArray[4] = ~~input;
  5631. break;
  5632. // SECOND
  5633. case 's' : // fall through to ss
  5634. case 'ss' :
  5635. datePartArray[5] = ~~input;
  5636. break;
  5637. // MILLISECOND
  5638. case 'S' :
  5639. case 'SS' :
  5640. case 'SSS' :
  5641. datePartArray[6] = ~~ (('0.' + input) * 1000);
  5642. break;
  5643. // UNIX TIMESTAMP WITH MS
  5644. case 'X':
  5645. config._d = new Date(parseFloat(input) * 1000);
  5646. break;
  5647. // TIMEZONE
  5648. case 'Z' : // fall through to ZZ
  5649. case 'ZZ' :
  5650. config._useUTC = true;
  5651. a = (input + '').match(parseTimezoneChunker);
  5652. if (a && a[1]) {
  5653. config._tzh = ~~a[1];
  5654. }
  5655. if (a && a[2]) {
  5656. config._tzm = ~~a[2];
  5657. }
  5658. // reverse offsets
  5659. if (a && a[0] === '+') {
  5660. config._tzh = -config._tzh;
  5661. config._tzm = -config._tzm;
  5662. }
  5663. break;
  5664. }
  5665. // if the input is null, the date is not valid
  5666. if (input == null) {
  5667. config._isValid = false;
  5668. }
  5669. }
  5670. // convert an array to a date.
  5671. // the array should mirror the parameters below
  5672. // note: all values past the year are optional and will default to the lowest possible value.
  5673. // [year, month, day , hour, minute, second, millisecond]
  5674. function dateFromArray(config) {
  5675. var i, date, input = [];
  5676. if (config._d) {
  5677. return;
  5678. }
  5679. for (i = 0; i < 7; i++) {
  5680. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  5681. }
  5682. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  5683. input[3] += config._tzh || 0;
  5684. input[4] += config._tzm || 0;
  5685. date = new Date(0);
  5686. if (config._useUTC) {
  5687. date.setUTCFullYear(input[0], input[1], input[2]);
  5688. date.setUTCHours(input[3], input[4], input[5], input[6]);
  5689. } else {
  5690. date.setFullYear(input[0], input[1], input[2]);
  5691. date.setHours(input[3], input[4], input[5], input[6]);
  5692. }
  5693. config._d = date;
  5694. }
  5695. // date from string and format string
  5696. function makeDateFromStringAndFormat(config) {
  5697. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  5698. var tokens = config._f.match(formattingTokens),
  5699. string = config._i,
  5700. i, parsedInput;
  5701. config._a = [];
  5702. for (i = 0; i < tokens.length; i++) {
  5703. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  5704. if (parsedInput) {
  5705. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  5706. }
  5707. // don't parse if its not a known token
  5708. if (formatTokenFunctions[tokens[i]]) {
  5709. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  5710. }
  5711. }
  5712. // handle am pm
  5713. if (config._isPm && config._a[3] < 12) {
  5714. config._a[3] += 12;
  5715. }
  5716. // if is 12 am, change hours to 0
  5717. if (config._isPm === false && config._a[3] === 12) {
  5718. config._a[3] = 0;
  5719. }
  5720. // return
  5721. dateFromArray(config);
  5722. }
  5723. // date from string and array of format strings
  5724. function makeDateFromStringAndArray(config) {
  5725. var tempConfig,
  5726. tempMoment,
  5727. bestMoment,
  5728. scoreToBeat = 99,
  5729. i,
  5730. currentDate,
  5731. currentScore;
  5732. while (config._f.length) {
  5733. tempConfig = extend({}, config);
  5734. tempConfig._f = config._f.pop();
  5735. makeDateFromStringAndFormat(tempConfig);
  5736. tempMoment = new Moment(tempConfig);
  5737. if (tempMoment.isValid()) {
  5738. bestMoment = tempMoment;
  5739. break;
  5740. }
  5741. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  5742. if (currentScore < scoreToBeat) {
  5743. scoreToBeat = currentScore;
  5744. bestMoment = tempMoment;
  5745. }
  5746. }
  5747. extend(config, bestMoment);
  5748. }
  5749. // date from iso format
  5750. function makeDateFromString(config) {
  5751. var i,
  5752. string = config._i;
  5753. if (isoRegex.exec(string)) {
  5754. config._f = 'YYYY-MM-DDT';
  5755. for (i = 0; i < 4; i++) {
  5756. if (isoTimes[i][1].exec(string)) {
  5757. config._f += isoTimes[i][0];
  5758. break;
  5759. }
  5760. }
  5761. if (parseTokenTimezone.exec(string)) {
  5762. config._f += " Z";
  5763. }
  5764. makeDateFromStringAndFormat(config);
  5765. } else {
  5766. config._d = new Date(string);
  5767. }
  5768. }
  5769. function makeDateFromInput(config) {
  5770. var input = config._i,
  5771. matched = aspNetJsonRegex.exec(input);
  5772. if (input === undefined) {
  5773. config._d = new Date();
  5774. } else if (matched) {
  5775. config._d = new Date(+matched[1]);
  5776. } else if (typeof input === 'string') {
  5777. makeDateFromString(config);
  5778. } else if (isArray(input)) {
  5779. config._a = input.slice(0);
  5780. dateFromArray(config);
  5781. } else {
  5782. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  5783. }
  5784. }
  5785. /************************************
  5786. Relative Time
  5787. ************************************/
  5788. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  5789. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  5790. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  5791. }
  5792. function relativeTime(milliseconds, withoutSuffix, lang) {
  5793. var seconds = round(Math.abs(milliseconds) / 1000),
  5794. minutes = round(seconds / 60),
  5795. hours = round(minutes / 60),
  5796. days = round(hours / 24),
  5797. years = round(days / 365),
  5798. args = seconds < 45 && ['s', seconds] ||
  5799. minutes === 1 && ['m'] ||
  5800. minutes < 45 && ['mm', minutes] ||
  5801. hours === 1 && ['h'] ||
  5802. hours < 22 && ['hh', hours] ||
  5803. days === 1 && ['d'] ||
  5804. days <= 25 && ['dd', days] ||
  5805. days <= 45 && ['M'] ||
  5806. days < 345 && ['MM', round(days / 30)] ||
  5807. years === 1 && ['y'] || ['yy', years];
  5808. args[2] = withoutSuffix;
  5809. args[3] = milliseconds > 0;
  5810. args[4] = lang;
  5811. return substituteTimeAgo.apply({}, args);
  5812. }
  5813. /************************************
  5814. Week of Year
  5815. ************************************/
  5816. // firstDayOfWeek 0 = sun, 6 = sat
  5817. // the day of the week that starts the week
  5818. // (usually sunday or monday)
  5819. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  5820. // the first week is the week that contains the first
  5821. // of this day of the week
  5822. // (eg. ISO weeks use thursday (4))
  5823. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  5824. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  5825. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  5826. if (daysToDayOfWeek > end) {
  5827. daysToDayOfWeek -= 7;
  5828. }
  5829. if (daysToDayOfWeek < end - 7) {
  5830. daysToDayOfWeek += 7;
  5831. }
  5832. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  5833. }
  5834. /************************************
  5835. Top Level Functions
  5836. ************************************/
  5837. function makeMoment(config) {
  5838. var input = config._i,
  5839. format = config._f;
  5840. if (input === null || input === '') {
  5841. return null;
  5842. }
  5843. if (typeof input === 'string') {
  5844. config._i = input = getLangDefinition().preparse(input);
  5845. }
  5846. if (moment.isMoment(input)) {
  5847. config = extend({}, input);
  5848. config._d = new Date(+input._d);
  5849. } else if (format) {
  5850. if (isArray(format)) {
  5851. makeDateFromStringAndArray(config);
  5852. } else {
  5853. makeDateFromStringAndFormat(config);
  5854. }
  5855. } else {
  5856. makeDateFromInput(config);
  5857. }
  5858. return new Moment(config);
  5859. }
  5860. moment = function (input, format, lang) {
  5861. return makeMoment({
  5862. _i : input,
  5863. _f : format,
  5864. _l : lang,
  5865. _isUTC : false
  5866. });
  5867. };
  5868. // creating with utc
  5869. moment.utc = function (input, format, lang) {
  5870. return makeMoment({
  5871. _useUTC : true,
  5872. _isUTC : true,
  5873. _l : lang,
  5874. _i : input,
  5875. _f : format
  5876. });
  5877. };
  5878. // creating with unix timestamp (in seconds)
  5879. moment.unix = function (input) {
  5880. return moment(input * 1000);
  5881. };
  5882. // duration
  5883. moment.duration = function (input, key) {
  5884. var isDuration = moment.isDuration(input),
  5885. isNumber = (typeof input === 'number'),
  5886. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  5887. ret;
  5888. if (isNumber) {
  5889. if (key) {
  5890. duration[key] = input;
  5891. } else {
  5892. duration.milliseconds = input;
  5893. }
  5894. }
  5895. ret = new Duration(duration);
  5896. if (isDuration && input.hasOwnProperty('_lang')) {
  5897. ret._lang = input._lang;
  5898. }
  5899. return ret;
  5900. };
  5901. // version number
  5902. moment.version = VERSION;
  5903. // default format
  5904. moment.defaultFormat = isoFormat;
  5905. // This function will load languages and then set the global language. If
  5906. // no arguments are passed in, it will simply return the current global
  5907. // language key.
  5908. moment.lang = function (key, values) {
  5909. var i;
  5910. if (!key) {
  5911. return moment.fn._lang._abbr;
  5912. }
  5913. if (values) {
  5914. loadLang(key, values);
  5915. } else if (!languages[key]) {
  5916. getLangDefinition(key);
  5917. }
  5918. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  5919. };
  5920. // returns language data
  5921. moment.langData = function (key) {
  5922. if (key && key._lang && key._lang._abbr) {
  5923. key = key._lang._abbr;
  5924. }
  5925. return getLangDefinition(key);
  5926. };
  5927. // compare moment object
  5928. moment.isMoment = function (obj) {
  5929. return obj instanceof Moment;
  5930. };
  5931. // for typechecking Duration objects
  5932. moment.isDuration = function (obj) {
  5933. return obj instanceof Duration;
  5934. };
  5935. /************************************
  5936. Moment Prototype
  5937. ************************************/
  5938. moment.fn = Moment.prototype = {
  5939. clone : function () {
  5940. return moment(this);
  5941. },
  5942. valueOf : function () {
  5943. return +this._d;
  5944. },
  5945. unix : function () {
  5946. return Math.floor(+this._d / 1000);
  5947. },
  5948. toString : function () {
  5949. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  5950. },
  5951. toDate : function () {
  5952. return this._d;
  5953. },
  5954. toJSON : function () {
  5955. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  5956. },
  5957. toArray : function () {
  5958. var m = this;
  5959. return [
  5960. m.year(),
  5961. m.month(),
  5962. m.date(),
  5963. m.hours(),
  5964. m.minutes(),
  5965. m.seconds(),
  5966. m.milliseconds()
  5967. ];
  5968. },
  5969. isValid : function () {
  5970. if (this._isValid == null) {
  5971. if (this._a) {
  5972. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  5973. } else {
  5974. this._isValid = !isNaN(this._d.getTime());
  5975. }
  5976. }
  5977. return !!this._isValid;
  5978. },
  5979. utc : function () {
  5980. this._isUTC = true;
  5981. return this;
  5982. },
  5983. local : function () {
  5984. this._isUTC = false;
  5985. return this;
  5986. },
  5987. format : function (inputString) {
  5988. var output = formatMoment(this, inputString || moment.defaultFormat);
  5989. return this.lang().postformat(output);
  5990. },
  5991. add : function (input, val) {
  5992. var dur;
  5993. // switch args to support add('s', 1) and add(1, 's')
  5994. if (typeof input === 'string') {
  5995. dur = moment.duration(+val, input);
  5996. } else {
  5997. dur = moment.duration(input, val);
  5998. }
  5999. addOrSubtractDurationFromMoment(this, dur, 1);
  6000. return this;
  6001. },
  6002. subtract : function (input, val) {
  6003. var dur;
  6004. // switch args to support subtract('s', 1) and subtract(1, 's')
  6005. if (typeof input === 'string') {
  6006. dur = moment.duration(+val, input);
  6007. } else {
  6008. dur = moment.duration(input, val);
  6009. }
  6010. addOrSubtractDurationFromMoment(this, dur, -1);
  6011. return this;
  6012. },
  6013. diff : function (input, units, asFloat) {
  6014. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  6015. zoneDiff = (this.zone() - that.zone()) * 6e4,
  6016. diff, output;
  6017. if (units) {
  6018. // standardize on singular form
  6019. units = units.replace(/s$/, '');
  6020. }
  6021. if (units === 'year' || units === 'month') {
  6022. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  6023. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  6024. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  6025. if (units === 'year') {
  6026. output = output / 12;
  6027. }
  6028. } else {
  6029. diff = (this - that) - zoneDiff;
  6030. output = units === 'second' ? diff / 1e3 : // 1000
  6031. units === 'minute' ? diff / 6e4 : // 1000 * 60
  6032. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  6033. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  6034. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  6035. diff;
  6036. }
  6037. return asFloat ? output : absRound(output);
  6038. },
  6039. from : function (time, withoutSuffix) {
  6040. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  6041. },
  6042. fromNow : function (withoutSuffix) {
  6043. return this.from(moment(), withoutSuffix);
  6044. },
  6045. calendar : function () {
  6046. var diff = this.diff(moment().startOf('day'), 'days', true),
  6047. format = diff < -6 ? 'sameElse' :
  6048. diff < -1 ? 'lastWeek' :
  6049. diff < 0 ? 'lastDay' :
  6050. diff < 1 ? 'sameDay' :
  6051. diff < 2 ? 'nextDay' :
  6052. diff < 7 ? 'nextWeek' : 'sameElse';
  6053. return this.format(this.lang().calendar(format, this));
  6054. },
  6055. isLeapYear : function () {
  6056. var year = this.year();
  6057. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  6058. },
  6059. isDST : function () {
  6060. return (this.zone() < moment([this.year()]).zone() ||
  6061. this.zone() < moment([this.year(), 5]).zone());
  6062. },
  6063. day : function (input) {
  6064. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  6065. return input == null ? day :
  6066. this.add({ d : input - day });
  6067. },
  6068. startOf: function (units) {
  6069. units = units.replace(/s$/, '');
  6070. // the following switch intentionally omits break keywords
  6071. // to utilize falling through the cases.
  6072. switch (units) {
  6073. case 'year':
  6074. this.month(0);
  6075. /* falls through */
  6076. case 'month':
  6077. this.date(1);
  6078. /* falls through */
  6079. case 'week':
  6080. case 'day':
  6081. this.hours(0);
  6082. /* falls through */
  6083. case 'hour':
  6084. this.minutes(0);
  6085. /* falls through */
  6086. case 'minute':
  6087. this.seconds(0);
  6088. /* falls through */
  6089. case 'second':
  6090. this.milliseconds(0);
  6091. /* falls through */
  6092. }
  6093. // weeks are a special case
  6094. if (units === 'week') {
  6095. this.day(0);
  6096. }
  6097. return this;
  6098. },
  6099. endOf: function (units) {
  6100. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  6101. },
  6102. isAfter: function (input, units) {
  6103. units = typeof units !== 'undefined' ? units : 'millisecond';
  6104. return +this.clone().startOf(units) > +moment(input).startOf(units);
  6105. },
  6106. isBefore: function (input, units) {
  6107. units = typeof units !== 'undefined' ? units : 'millisecond';
  6108. return +this.clone().startOf(units) < +moment(input).startOf(units);
  6109. },
  6110. isSame: function (input, units) {
  6111. units = typeof units !== 'undefined' ? units : 'millisecond';
  6112. return +this.clone().startOf(units) === +moment(input).startOf(units);
  6113. },
  6114. zone : function () {
  6115. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  6116. },
  6117. daysInMonth : function () {
  6118. return moment.utc([this.year(), this.month() + 1, 0]).date();
  6119. },
  6120. dayOfYear : function (input) {
  6121. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  6122. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  6123. },
  6124. isoWeek : function (input) {
  6125. var week = weekOfYear(this, 1, 4);
  6126. return input == null ? week : this.add("d", (input - week) * 7);
  6127. },
  6128. week : function (input) {
  6129. var week = this.lang().week(this);
  6130. return input == null ? week : this.add("d", (input - week) * 7);
  6131. },
  6132. // If passed a language key, it will set the language for this
  6133. // instance. Otherwise, it will return the language configuration
  6134. // variables for this instance.
  6135. lang : function (key) {
  6136. if (key === undefined) {
  6137. return this._lang;
  6138. } else {
  6139. this._lang = getLangDefinition(key);
  6140. return this;
  6141. }
  6142. }
  6143. };
  6144. // helper for adding shortcuts
  6145. function makeGetterAndSetter(name, key) {
  6146. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  6147. var utc = this._isUTC ? 'UTC' : '';
  6148. if (input != null) {
  6149. this._d['set' + utc + key](input);
  6150. return this;
  6151. } else {
  6152. return this._d['get' + utc + key]();
  6153. }
  6154. };
  6155. }
  6156. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  6157. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  6158. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  6159. }
  6160. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  6161. makeGetterAndSetter('year', 'FullYear');
  6162. // add plural methods
  6163. moment.fn.days = moment.fn.day;
  6164. moment.fn.weeks = moment.fn.week;
  6165. moment.fn.isoWeeks = moment.fn.isoWeek;
  6166. /************************************
  6167. Duration Prototype
  6168. ************************************/
  6169. moment.duration.fn = Duration.prototype = {
  6170. weeks : function () {
  6171. return absRound(this.days() / 7);
  6172. },
  6173. valueOf : function () {
  6174. return this._milliseconds +
  6175. this._days * 864e5 +
  6176. this._months * 2592e6;
  6177. },
  6178. humanize : function (withSuffix) {
  6179. var difference = +this,
  6180. output = relativeTime(difference, !withSuffix, this.lang());
  6181. if (withSuffix) {
  6182. output = this.lang().pastFuture(difference, output);
  6183. }
  6184. return this.lang().postformat(output);
  6185. },
  6186. lang : moment.fn.lang
  6187. };
  6188. function makeDurationGetter(name) {
  6189. moment.duration.fn[name] = function () {
  6190. return this._data[name];
  6191. };
  6192. }
  6193. function makeDurationAsGetter(name, factor) {
  6194. moment.duration.fn['as' + name] = function () {
  6195. return +this / factor;
  6196. };
  6197. }
  6198. for (i in unitMillisecondFactors) {
  6199. if (unitMillisecondFactors.hasOwnProperty(i)) {
  6200. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  6201. makeDurationGetter(i.toLowerCase());
  6202. }
  6203. }
  6204. makeDurationAsGetter('Weeks', 6048e5);
  6205. /************************************
  6206. Default Lang
  6207. ************************************/
  6208. // Set default language, other languages will inherit from English.
  6209. moment.lang('en', {
  6210. ordinal : function (number) {
  6211. var b = number % 10,
  6212. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  6213. (b === 1) ? 'st' :
  6214. (b === 2) ? 'nd' :
  6215. (b === 3) ? 'rd' : 'th';
  6216. return number + output;
  6217. }
  6218. });
  6219. /************************************
  6220. Exposing Moment
  6221. ************************************/
  6222. // CommonJS module is defined
  6223. if (hasModule) {
  6224. module.exports = moment;
  6225. }
  6226. /*global ender:false */
  6227. if (typeof ender === 'undefined') {
  6228. // here, `this` means `window` in the browser, or `global` on the server
  6229. // add `moment` as a global object via a string identifier,
  6230. // for Closure Compiler "advanced" mode
  6231. this['moment'] = moment;
  6232. }
  6233. /*global define:false */
  6234. if (typeof define === "function" && define.amd) {
  6235. define("moment", [], function () {
  6236. return moment;
  6237. });
  6238. }
  6239. }).call(this);
  6240. })()
  6241. },{}]},{},[1])(1)
  6242. });
  6243. ;