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.

909 lines
30 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
  1. var Emitter = require('emitter-component');
  2. var Hammer = require('hammerjs');
  3. var util = require('../util');
  4. var DataSet = require('../DataSet');
  5. var DataView = require('../DataView');
  6. var Range = require('./Range');
  7. var TimeAxis = require('./component/TimeAxis');
  8. var CurrentTime = require('./component/CurrentTime');
  9. var CustomTime = require('./component/CustomTime');
  10. var ItemSet = require('./component/ItemSet');
  11. /**
  12. * Create a timeline visualization
  13. * @param {HTMLElement} container
  14. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  15. * @param {Object} [options] See Timeline.setOptions for the available options.
  16. * @constructor
  17. */
  18. function Timeline (container, items, options) {
  19. if (!(this instanceof Timeline)) {
  20. throw new SyntaxError('Constructor must be called with the new operator');
  21. }
  22. var me = this;
  23. this.defaultOptions = {
  24. start: null,
  25. end: null,
  26. autoResize: true,
  27. orientation: 'bottom',
  28. width: null,
  29. height: null,
  30. maxHeight: null,
  31. minHeight: null
  32. };
  33. this.options = util.deepExtend({}, this.defaultOptions);
  34. // Create the DOM, props, and emitter
  35. this._create(container);
  36. // all components listed here will be repainted automatically
  37. this.components = [];
  38. this.body = {
  39. dom: this.dom,
  40. domProps: this.props,
  41. emitter: {
  42. on: this.on.bind(this),
  43. off: this.off.bind(this),
  44. emit: this.emit.bind(this)
  45. },
  46. util: {
  47. snap: null, // will be specified after TimeAxis is created
  48. toScreen: me._toScreen.bind(me),
  49. toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
  50. toTime: me._toTime.bind(me),
  51. toGlobalTime : me._toGlobalTime.bind(me)
  52. }
  53. };
  54. // range
  55. this.range = new Range(this.body);
  56. this.components.push(this.range);
  57. this.body.range = this.range;
  58. // time axis
  59. this.timeAxis = new TimeAxis(this.body);
  60. this.components.push(this.timeAxis);
  61. this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
  62. // current time bar
  63. this.currentTime = new CurrentTime(this.body);
  64. this.components.push(this.currentTime);
  65. // custom time bar
  66. // Note: time bar will be attached in this.setOptions when selected
  67. this.customTime = new CustomTime(this.body);
  68. this.components.push(this.customTime);
  69. // item set
  70. this.itemSet = new ItemSet(this.body);
  71. this.components.push(this.itemSet);
  72. this.itemsData = null; // DataSet
  73. this.groupsData = null; // DataSet
  74. // apply options
  75. if (options) {
  76. this.setOptions(options);
  77. }
  78. // create itemset
  79. if (items) {
  80. this.setItems(items);
  81. }
  82. else {
  83. this.redraw();
  84. }
  85. }
  86. // turn Timeline into an event emitter
  87. Emitter(Timeline.prototype);
  88. /**
  89. * Create the main DOM for the Timeline: a root panel containing left, right,
  90. * top, bottom, content, and background panel.
  91. * @param {Element} container The container element where the Timeline will
  92. * be attached.
  93. * @private
  94. */
  95. Timeline.prototype._create = function (container) {
  96. this.dom = {};
  97. this.dom.root = document.createElement('div');
  98. this.dom.background = document.createElement('div');
  99. this.dom.backgroundVertical = document.createElement('div');
  100. this.dom.backgroundHorizontal = document.createElement('div');
  101. this.dom.centerContainer = document.createElement('div');
  102. this.dom.leftContainer = document.createElement('div');
  103. this.dom.rightContainer = document.createElement('div');
  104. this.dom.center = document.createElement('div');
  105. this.dom.left = document.createElement('div');
  106. this.dom.right = document.createElement('div');
  107. this.dom.top = document.createElement('div');
  108. this.dom.bottom = document.createElement('div');
  109. this.dom.shadowTop = document.createElement('div');
  110. this.dom.shadowBottom = document.createElement('div');
  111. this.dom.shadowTopLeft = document.createElement('div');
  112. this.dom.shadowBottomLeft = document.createElement('div');
  113. this.dom.shadowTopRight = document.createElement('div');
  114. this.dom.shadowBottomRight = document.createElement('div');
  115. this.dom.background.className = 'vispanel background';
  116. this.dom.backgroundVertical.className = 'vispanel background vertical';
  117. this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
  118. this.dom.centerContainer.className = 'vispanel center';
  119. this.dom.leftContainer.className = 'vispanel left';
  120. this.dom.rightContainer.className = 'vispanel right';
  121. this.dom.top.className = 'vispanel top';
  122. this.dom.bottom.className = 'vispanel bottom';
  123. this.dom.left.className = 'content';
  124. this.dom.center.className = 'content';
  125. this.dom.right.className = 'content';
  126. this.dom.shadowTop.className = 'shadow top';
  127. this.dom.shadowBottom.className = 'shadow bottom';
  128. this.dom.shadowTopLeft.className = 'shadow top';
  129. this.dom.shadowBottomLeft.className = 'shadow bottom';
  130. this.dom.shadowTopRight.className = 'shadow top';
  131. this.dom.shadowBottomRight.className = 'shadow bottom';
  132. this.dom.root.appendChild(this.dom.background);
  133. this.dom.root.appendChild(this.dom.backgroundVertical);
  134. this.dom.root.appendChild(this.dom.backgroundHorizontal);
  135. this.dom.root.appendChild(this.dom.centerContainer);
  136. this.dom.root.appendChild(this.dom.leftContainer);
  137. this.dom.root.appendChild(this.dom.rightContainer);
  138. this.dom.root.appendChild(this.dom.top);
  139. this.dom.root.appendChild(this.dom.bottom);
  140. this.dom.centerContainer.appendChild(this.dom.center);
  141. this.dom.leftContainer.appendChild(this.dom.left);
  142. this.dom.rightContainer.appendChild(this.dom.right);
  143. this.dom.centerContainer.appendChild(this.dom.shadowTop);
  144. this.dom.centerContainer.appendChild(this.dom.shadowBottom);
  145. this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
  146. this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
  147. this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
  148. this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
  149. this.on('rangechange', this.redraw.bind(this));
  150. this.on('change', this.redraw.bind(this));
  151. this.on('touch', this._onTouch.bind(this));
  152. this.on('pinch', this._onPinch.bind(this));
  153. this.on('dragstart', this._onDragStart.bind(this));
  154. this.on('drag', this._onDrag.bind(this));
  155. // create event listeners for all interesting events, these events will be
  156. // emitted via emitter
  157. this.hammer = Hammer(this.dom.root, {
  158. prevent_default: true
  159. });
  160. this.listeners = {};
  161. var me = this;
  162. var events = [
  163. 'touch', 'pinch',
  164. 'tap', 'doubletap', 'hold',
  165. 'dragstart', 'drag', 'dragend',
  166. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
  167. ];
  168. events.forEach(function (event) {
  169. var listener = function () {
  170. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  171. me.emit.apply(me, args);
  172. };
  173. me.hammer.on(event, listener);
  174. me.listeners[event] = listener;
  175. });
  176. // size properties of each of the panels
  177. this.props = {
  178. root: {},
  179. background: {},
  180. centerContainer: {},
  181. leftContainer: {},
  182. rightContainer: {},
  183. center: {},
  184. left: {},
  185. right: {},
  186. top: {},
  187. bottom: {},
  188. border: {},
  189. scrollTop: 0,
  190. scrollTopMin: 0
  191. };
  192. this.touch = {}; // store state information needed for touch events
  193. // attach the root panel to the provided container
  194. if (!container) throw new Error('No container provided');
  195. container.appendChild(this.dom.root);
  196. };
  197. /**
  198. * Destroy the Timeline, clean up all DOM elements and event listeners.
  199. */
  200. Timeline.prototype.destroy = function () {
  201. // unbind datasets
  202. this.clear();
  203. // remove all event listeners
  204. this.off();
  205. // stop checking for changed size
  206. this._stopAutoResize();
  207. // remove from DOM
  208. if (this.dom.root.parentNode) {
  209. this.dom.root.parentNode.removeChild(this.dom.root);
  210. }
  211. this.dom = null;
  212. // cleanup hammer touch events
  213. for (var event in this.listeners) {
  214. if (this.listeners.hasOwnProperty(event)) {
  215. delete this.listeners[event];
  216. }
  217. }
  218. this.listeners = null;
  219. this.hammer = null;
  220. // give all components the opportunity to cleanup
  221. this.components.forEach(function (component) {
  222. component.destroy();
  223. });
  224. this.body = null;
  225. };
  226. /**
  227. * Set options. Options will be passed to all components loaded in the Timeline.
  228. * @param {Object} [options]
  229. * {String} orientation
  230. * Vertical orientation for the Timeline,
  231. * can be 'bottom' (default) or 'top'.
  232. * {String | Number} width
  233. * Width for the timeline, a number in pixels or
  234. * a css string like '1000px' or '75%'. '100%' by default.
  235. * {String | Number} height
  236. * Fixed height for the Timeline, a number in pixels or
  237. * a css string like '400px' or '75%'. If undefined,
  238. * The Timeline will automatically size such that
  239. * its contents fit.
  240. * {String | Number} minHeight
  241. * Minimum height for the Timeline, a number in pixels or
  242. * a css string like '400px' or '75%'.
  243. * {String | Number} maxHeight
  244. * Maximum height for the Timeline, a number in pixels or
  245. * a css string like '400px' or '75%'.
  246. * {Number | Date | String} start
  247. * Start date for the visible window
  248. * {Number | Date | String} end
  249. * End date for the visible window
  250. */
  251. Timeline.prototype.setOptions = function (options) {
  252. if (options) {
  253. // copy the known options
  254. var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
  255. util.selectiveExtend(fields, this.options, options);
  256. // enable/disable autoResize
  257. this._initAutoResize();
  258. }
  259. // propagate options to all components
  260. this.components.forEach(function (component) {
  261. component.setOptions(options);
  262. });
  263. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  264. if (options && options.order) {
  265. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  266. }
  267. // redraw everything
  268. this.redraw();
  269. };
  270. /**
  271. * Set a custom time bar
  272. * @param {Date} time
  273. */
  274. Timeline.prototype.setCustomTime = function (time) {
  275. if (!this.customTime) {
  276. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  277. }
  278. this.customTime.setCustomTime(time);
  279. };
  280. /**
  281. * Retrieve the current custom time.
  282. * @return {Date} customTime
  283. */
  284. Timeline.prototype.getCustomTime = function() {
  285. if (!this.customTime) {
  286. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  287. }
  288. return this.customTime.getCustomTime();
  289. };
  290. /**
  291. * Set items
  292. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  293. */
  294. Timeline.prototype.setItems = function(items) {
  295. var initialLoad = (this.itemsData == null);
  296. // convert to type DataSet when needed
  297. var newDataSet;
  298. if (!items) {
  299. newDataSet = null;
  300. }
  301. else if (items instanceof DataSet || items instanceof DataView) {
  302. newDataSet = items;
  303. }
  304. else {
  305. // turn an array into a dataset
  306. newDataSet = new DataSet(items, {
  307. type: {
  308. start: 'Date',
  309. end: 'Date'
  310. }
  311. });
  312. }
  313. // set items
  314. this.itemsData = newDataSet;
  315. this.itemSet && this.itemSet.setItems(newDataSet);
  316. if (initialLoad && ('start' in this.options || 'end' in this.options)) {
  317. this.fit();
  318. var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
  319. var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
  320. this.setWindow(start, end);
  321. }
  322. };
  323. /**
  324. * Get the id's of the currently visible items.
  325. * @returns {Array} The ids of the visible items
  326. */
  327. Timeline.prototype.getVisibleItems = function() {
  328. return this.itemSet && this.itemSet.getVisibleItems() || [];
  329. };
  330. /**
  331. * Set groups
  332. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  333. */
  334. Timeline.prototype.setGroups = function(groups) {
  335. // convert to type DataSet when needed
  336. var newDataSet;
  337. if (!groups) {
  338. newDataSet = null;
  339. }
  340. else if (groups instanceof DataSet || groups instanceof DataView) {
  341. newDataSet = groups;
  342. }
  343. else {
  344. // turn an array into a dataset
  345. newDataSet = new DataSet(groups);
  346. }
  347. this.groupsData = newDataSet;
  348. this.itemSet.setGroups(newDataSet);
  349. };
  350. /**
  351. * Clear the Timeline. By Default, items, groups and options are cleared.
  352. * Example usage:
  353. *
  354. * timeline.clear(); // clear items, groups, and options
  355. * timeline.clear({options: true}); // clear options only
  356. *
  357. * @param {Object} [what] Optionally specify what to clear. By default:
  358. * {items: true, groups: true, options: true}
  359. */
  360. Timeline.prototype.clear = function(what) {
  361. // clear items
  362. if (!what || what.items) {
  363. this.setItems(null);
  364. }
  365. // clear groups
  366. if (!what || what.groups) {
  367. this.setGroups(null);
  368. }
  369. // clear options of timeline and of each of the components
  370. if (!what || what.options) {
  371. this.components.forEach(function (component) {
  372. component.setOptions(component.defaultOptions);
  373. });
  374. this.setOptions(this.defaultOptions); // this will also do a redraw
  375. }
  376. };
  377. /**
  378. * Set Timeline window such that it fits all items
  379. */
  380. Timeline.prototype.fit = function() {
  381. // apply the data range as range
  382. var dataRange = this.getItemRange();
  383. // add 5% space on both sides
  384. var start = dataRange.min;
  385. var end = dataRange.max;
  386. if (start != null && end != null) {
  387. var interval = (end.valueOf() - start.valueOf());
  388. if (interval <= 0) {
  389. // prevent an empty interval
  390. interval = 24 * 60 * 60 * 1000; // 1 day
  391. }
  392. start = new Date(start.valueOf() - interval * 0.05);
  393. end = new Date(end.valueOf() + interval * 0.05);
  394. }
  395. // skip range set if there is no start and end date
  396. if (start === null && end === null) {
  397. return;
  398. }
  399. this.range.setRange(start, end);
  400. };
  401. /**
  402. * Get the data range of the item set.
  403. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  404. * When no minimum is found, min==null
  405. * When no maximum is found, max==null
  406. */
  407. Timeline.prototype.getItemRange = function() {
  408. // calculate min from start filed
  409. var dataset = this.itemsData.getDataSet(),
  410. min = null,
  411. max = null;
  412. if (dataset) {
  413. // calculate the minimum value of the field 'start'
  414. var minItem = dataset.min('start');
  415. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  416. // Note: we convert first to Date and then to number because else
  417. // a conversion from ISODate to Number will fail
  418. // calculate maximum value of fields 'start' and 'end'
  419. var maxStartItem = dataset.max('start');
  420. if (maxStartItem) {
  421. max = util.convert(maxStartItem.start, 'Date').valueOf();
  422. }
  423. var maxEndItem = dataset.max('end');
  424. if (maxEndItem) {
  425. if (max == null) {
  426. max = util.convert(maxEndItem.end, 'Date').valueOf();
  427. }
  428. else {
  429. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  430. }
  431. }
  432. }
  433. return {
  434. min: (min != null) ? new Date(min) : null,
  435. max: (max != null) ? new Date(max) : null
  436. };
  437. };
  438. /**
  439. * Set selected items by their id. Replaces the current selection
  440. * Unknown id's are silently ignored.
  441. * @param {Array} [ids] An array with zero or more id's of the items to be
  442. * selected. If ids is an empty array, all items will be
  443. * unselected.
  444. */
  445. Timeline.prototype.setSelection = function(ids) {
  446. this.itemSet && this.itemSet.setSelection(ids);
  447. };
  448. /**
  449. * Get the selected items by their id
  450. * @return {Array} ids The ids of the selected items
  451. */
  452. Timeline.prototype.getSelection = function() {
  453. return this.itemSet && this.itemSet.getSelection() || [];
  454. };
  455. /**
  456. * Set the visible window. Both parameters are optional, you can change only
  457. * start or only end. Syntax:
  458. *
  459. * TimeLine.setWindow(start, end)
  460. * TimeLine.setWindow(range)
  461. *
  462. * Where start and end can be a Date, number, or string, and range is an
  463. * object with properties start and end.
  464. *
  465. * @param {Date | Number | String | Object} [start] Start date of visible window
  466. * @param {Date | Number | String} [end] End date of visible window
  467. */
  468. Timeline.prototype.setWindow = function(start, end) {
  469. if (arguments.length == 1) {
  470. var range = arguments[0];
  471. this.range.setRange(range.start, range.end);
  472. }
  473. else {
  474. this.range.setRange(start, end);
  475. }
  476. };
  477. /**
  478. * Get the visible window
  479. * @return {{start: Date, end: Date}} Visible range
  480. */
  481. Timeline.prototype.getWindow = function() {
  482. var range = this.range.getRange();
  483. return {
  484. start: new Date(range.start),
  485. end: new Date(range.end)
  486. };
  487. };
  488. /**
  489. * Force a redraw of the Timeline. Can be useful to manually redraw when
  490. * option autoResize=false
  491. */
  492. Timeline.prototype.redraw = function() {
  493. var resized = false,
  494. options = this.options,
  495. props = this.props,
  496. dom = this.dom;
  497. if (!dom) return; // when destroyed
  498. // update class names
  499. dom.root.className = 'vis timeline root ' + options.orientation;
  500. // update root width and height options
  501. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  502. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  503. dom.root.style.width = util.option.asSize(options.width, '');
  504. // calculate border widths
  505. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  506. props.border.right = props.border.left;
  507. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  508. props.border.bottom = props.border.top;
  509. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  510. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  511. // calculate the heights. If any of the side panels is empty, we set the height to
  512. // minus the border width, such that the border will be invisible
  513. props.center.height = dom.center.offsetHeight;
  514. props.left.height = dom.left.offsetHeight;
  515. props.right.height = dom.right.offsetHeight;
  516. props.top.height = dom.top.clientHeight || -props.border.top;
  517. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  518. // TODO: compensate borders when any of the panels is empty.
  519. // apply auto height
  520. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  521. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  522. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  523. borderRootHeight + props.border.top + props.border.bottom;
  524. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  525. // calculate heights of the content panels
  526. props.root.height = dom.root.offsetHeight;
  527. props.background.height = props.root.height - borderRootHeight;
  528. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  529. borderRootHeight;
  530. props.centerContainer.height = containerHeight;
  531. props.leftContainer.height = containerHeight;
  532. props.rightContainer.height = props.leftContainer.height;
  533. // calculate the widths of the panels
  534. props.root.width = dom.root.offsetWidth;
  535. props.background.width = props.root.width - borderRootWidth;
  536. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  537. props.leftContainer.width = props.left.width;
  538. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  539. props.rightContainer.width = props.right.width;
  540. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  541. props.center.width = centerWidth;
  542. props.centerContainer.width = centerWidth;
  543. props.top.width = centerWidth;
  544. props.bottom.width = centerWidth;
  545. // resize the panels
  546. dom.background.style.height = props.background.height + 'px';
  547. dom.backgroundVertical.style.height = props.background.height + 'px';
  548. dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
  549. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  550. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  551. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  552. dom.background.style.width = props.background.width + 'px';
  553. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  554. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  555. dom.centerContainer.style.width = props.center.width + 'px';
  556. dom.top.style.width = props.top.width + 'px';
  557. dom.bottom.style.width = props.bottom.width + 'px';
  558. // reposition the panels
  559. dom.background.style.left = '0';
  560. dom.background.style.top = '0';
  561. dom.backgroundVertical.style.left = props.left.width + 'px';
  562. dom.backgroundVertical.style.top = '0';
  563. dom.backgroundHorizontal.style.left = '0';
  564. dom.backgroundHorizontal.style.top = props.top.height + 'px';
  565. dom.centerContainer.style.left = props.left.width + 'px';
  566. dom.centerContainer.style.top = props.top.height + 'px';
  567. dom.leftContainer.style.left = '0';
  568. dom.leftContainer.style.top = props.top.height + 'px';
  569. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  570. dom.rightContainer.style.top = props.top.height + 'px';
  571. dom.top.style.left = props.left.width + 'px';
  572. dom.top.style.top = '0';
  573. dom.bottom.style.left = props.left.width + 'px';
  574. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  575. // update the scrollTop, feasible range for the offset can be changed
  576. // when the height of the Timeline or of the contents of the center changed
  577. this._updateScrollTop();
  578. // reposition the scrollable contents
  579. var offset = this.props.scrollTop;
  580. if (options.orientation == 'bottom') {
  581. offset += Math.max(this.props.centerContainer.height - this.props.center.height -
  582. this.props.border.top - this.props.border.bottom, 0);
  583. }
  584. dom.center.style.left = '0';
  585. dom.center.style.top = offset + 'px';
  586. dom.left.style.left = '0';
  587. dom.left.style.top = offset + 'px';
  588. dom.right.style.left = '0';
  589. dom.right.style.top = offset + 'px';
  590. // show shadows when vertical scrolling is available
  591. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  592. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  593. dom.shadowTop.style.visibility = visibilityTop;
  594. dom.shadowBottom.style.visibility = visibilityBottom;
  595. dom.shadowTopLeft.style.visibility = visibilityTop;
  596. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  597. dom.shadowTopRight.style.visibility = visibilityTop;
  598. dom.shadowBottomRight.style.visibility = visibilityBottom;
  599. // redraw all components
  600. this.components.forEach(function (component) {
  601. resized = component.redraw() || resized;
  602. });
  603. if (resized) {
  604. // keep repainting until all sizes are settled
  605. this.redraw();
  606. }
  607. };
  608. // TODO: deprecated since version 1.1.0, remove some day
  609. Timeline.prototype.repaint = function () {
  610. throw new Error('Function repaint is deprecated. Use redraw instead.');
  611. };
  612. /**
  613. * Convert a position on screen (pixels) to a datetime
  614. * @param {int} x Position on the screen in pixels
  615. * @return {Date} time The datetime the corresponds with given position x
  616. * @private
  617. */
  618. // TODO: move this function to Range
  619. Timeline.prototype._toTime = function(x) {
  620. var conversion = this.range.conversion(this.props.center.width);
  621. return new Date(x / conversion.scale + conversion.offset);
  622. };
  623. /**
  624. * Convert a position on the global screen (pixels) to a datetime
  625. * @param {int} x Position on the screen in pixels
  626. * @return {Date} time The datetime the corresponds with given position x
  627. * @private
  628. */
  629. // TODO: move this function to Range
  630. Timeline.prototype._toGlobalTime = function(x) {
  631. var conversion = this.range.conversion(this.props.root.width);
  632. return new Date(x / conversion.scale + conversion.offset);
  633. };
  634. /**
  635. * Convert a datetime (Date object) into a position on the screen
  636. * @param {Date} time A date
  637. * @return {int} x The position on the screen in pixels which corresponds
  638. * with the given date.
  639. * @private
  640. */
  641. // TODO: move this function to Range
  642. Timeline.prototype._toScreen = function(time) {
  643. var conversion = this.range.conversion(this.props.center.width);
  644. return (time.valueOf() - conversion.offset) * conversion.scale;
  645. };
  646. /**
  647. * Convert a datetime (Date object) into a position on the root
  648. * This is used to get the pixel density estimate for the screen, not the center panel
  649. * @param {Date} time A date
  650. * @return {int} x The position on root in pixels which corresponds
  651. * with the given date.
  652. * @private
  653. */
  654. // TODO: move this function to Range
  655. Timeline.prototype._toGlobalScreen = function(time) {
  656. var conversion = this.range.conversion(this.props.root.width);
  657. return (time.valueOf() - conversion.offset) * conversion.scale;
  658. };
  659. /**
  660. * Initialize watching when option autoResize is true
  661. * @private
  662. */
  663. Timeline.prototype._initAutoResize = function () {
  664. if (this.options.autoResize == true) {
  665. this._startAutoResize();
  666. }
  667. else {
  668. this._stopAutoResize();
  669. }
  670. };
  671. /**
  672. * Watch for changes in the size of the container. On resize, the Panel will
  673. * automatically redraw itself.
  674. * @private
  675. */
  676. Timeline.prototype._startAutoResize = function () {
  677. var me = this;
  678. this._stopAutoResize();
  679. this._onResize = function() {
  680. if (me.options.autoResize != true) {
  681. // stop watching when the option autoResize is changed to false
  682. me._stopAutoResize();
  683. return;
  684. }
  685. if (me.dom.root) {
  686. // check whether the frame is resized
  687. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  688. (me.dom.root.clientHeight != me.props.lastHeight)) {
  689. me.props.lastWidth = me.dom.root.clientWidth;
  690. me.props.lastHeight = me.dom.root.clientHeight;
  691. me.emit('change');
  692. }
  693. }
  694. };
  695. // add event listener to window resize
  696. util.addEventListener(window, 'resize', this._onResize);
  697. this.watchTimer = setInterval(this._onResize, 1000);
  698. };
  699. /**
  700. * Stop watching for a resize of the frame.
  701. * @private
  702. */
  703. Timeline.prototype._stopAutoResize = function () {
  704. if (this.watchTimer) {
  705. clearInterval(this.watchTimer);
  706. this.watchTimer = undefined;
  707. }
  708. // remove event listener on window.resize
  709. util.removeEventListener(window, 'resize', this._onResize);
  710. this._onResize = null;
  711. };
  712. /**
  713. * Start moving the timeline vertically
  714. * @param {Event} event
  715. * @private
  716. */
  717. Timeline.prototype._onTouch = function (event) {
  718. this.touch.allowDragging = true;
  719. };
  720. /**
  721. * Start moving the timeline vertically
  722. * @param {Event} event
  723. * @private
  724. */
  725. Timeline.prototype._onPinch = function (event) {
  726. this.touch.allowDragging = false;
  727. };
  728. /**
  729. * Start moving the timeline vertically
  730. * @param {Event} event
  731. * @private
  732. */
  733. Timeline.prototype._onDragStart = function (event) {
  734. this.touch.initialScrollTop = this.props.scrollTop;
  735. };
  736. /**
  737. * Move the timeline vertically
  738. * @param {Event} event
  739. * @private
  740. */
  741. Timeline.prototype._onDrag = function (event) {
  742. // refuse to drag when we where pinching to prevent the timeline make a jump
  743. // when releasing the fingers in opposite order from the touch screen
  744. if (!this.touch.allowDragging) return;
  745. var delta = event.gesture.deltaY;
  746. var oldScrollTop = this._getScrollTop();
  747. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  748. if (newScrollTop != oldScrollTop) {
  749. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  750. }
  751. };
  752. /**
  753. * Apply a scrollTop
  754. * @param {Number} scrollTop
  755. * @returns {Number} scrollTop Returns the applied scrollTop
  756. * @private
  757. */
  758. Timeline.prototype._setScrollTop = function (scrollTop) {
  759. this.props.scrollTop = scrollTop;
  760. this._updateScrollTop();
  761. return this.props.scrollTop;
  762. };
  763. /**
  764. * Update the current scrollTop when the height of the containers has been changed
  765. * @returns {Number} scrollTop Returns the applied scrollTop
  766. * @private
  767. */
  768. Timeline.prototype._updateScrollTop = function () {
  769. // recalculate the scrollTopMin
  770. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  771. if (scrollTopMin != this.props.scrollTopMin) {
  772. // in case of bottom orientation, change the scrollTop such that the contents
  773. // do not move relative to the time axis at the bottom
  774. if (this.options.orientation == 'bottom') {
  775. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  776. }
  777. this.props.scrollTopMin = scrollTopMin;
  778. }
  779. // limit the scrollTop to the feasible scroll range
  780. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  781. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  782. return this.props.scrollTop;
  783. };
  784. /**
  785. * Get the current scrollTop
  786. * @returns {number} scrollTop
  787. * @private
  788. */
  789. Timeline.prototype._getScrollTop = function () {
  790. return this.props.scrollTop;
  791. };
  792. module.exports = Timeline;