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.

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