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.

900 lines
29 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. * Set groups
  325. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  326. */
  327. Timeline.prototype.setGroups = function(groups) {
  328. // convert to type DataSet when needed
  329. var newDataSet;
  330. if (!groups) {
  331. newDataSet = null;
  332. }
  333. else if (groups instanceof DataSet || groups instanceof DataView) {
  334. newDataSet = groups;
  335. }
  336. else {
  337. // turn an array into a dataset
  338. newDataSet = new DataSet(groups);
  339. }
  340. this.groupsData = newDataSet;
  341. this.itemSet.setGroups(newDataSet);
  342. };
  343. /**
  344. * Clear the Timeline. By Default, items, groups and options are cleared.
  345. * Example usage:
  346. *
  347. * timeline.clear(); // clear items, groups, and options
  348. * timeline.clear({options: true}); // clear options only
  349. *
  350. * @param {Object} [what] Optionally specify what to clear. By default:
  351. * {items: true, groups: true, options: true}
  352. */
  353. Timeline.prototype.clear = function(what) {
  354. // clear items
  355. if (!what || what.items) {
  356. this.setItems(null);
  357. }
  358. // clear groups
  359. if (!what || what.groups) {
  360. this.setGroups(null);
  361. }
  362. // clear options of timeline and of each of the components
  363. if (!what || what.options) {
  364. this.components.forEach(function (component) {
  365. component.setOptions(component.defaultOptions);
  366. });
  367. this.setOptions(this.defaultOptions); // this will also do a redraw
  368. }
  369. };
  370. /**
  371. * Set Timeline window such that it fits all items
  372. */
  373. Timeline.prototype.fit = function() {
  374. // apply the data range as range
  375. var dataRange = this.getItemRange();
  376. // add 5% space on both sides
  377. var start = dataRange.min;
  378. var end = dataRange.max;
  379. if (start != null && end != null) {
  380. var interval = (end.valueOf() - start.valueOf());
  381. if (interval <= 0) {
  382. // prevent an empty interval
  383. interval = 24 * 60 * 60 * 1000; // 1 day
  384. }
  385. start = new Date(start.valueOf() - interval * 0.05);
  386. end = new Date(end.valueOf() + interval * 0.05);
  387. }
  388. // skip range set if there is no start and end date
  389. if (start === null && end === null) {
  390. return;
  391. }
  392. this.range.setRange(start, end);
  393. };
  394. /**
  395. * Get the data range of the item set.
  396. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  397. * When no minimum is found, min==null
  398. * When no maximum is found, max==null
  399. */
  400. Timeline.prototype.getItemRange = function() {
  401. // calculate min from start filed
  402. var dataset = this.itemsData.getDataSet(),
  403. min = null,
  404. max = null;
  405. if (dataset) {
  406. // calculate the minimum value of the field 'start'
  407. var minItem = dataset.min('start');
  408. min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
  409. // Note: we convert first to Date and then to number because else
  410. // a conversion from ISODate to Number will fail
  411. // calculate maximum value of fields 'start' and 'end'
  412. var maxStartItem = dataset.max('start');
  413. if (maxStartItem) {
  414. max = util.convert(maxStartItem.start, 'Date').valueOf();
  415. }
  416. var maxEndItem = dataset.max('end');
  417. if (maxEndItem) {
  418. if (max == null) {
  419. max = util.convert(maxEndItem.end, 'Date').valueOf();
  420. }
  421. else {
  422. max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
  423. }
  424. }
  425. }
  426. return {
  427. min: (min != null) ? new Date(min) : null,
  428. max: (max != null) ? new Date(max) : null
  429. };
  430. };
  431. /**
  432. * Set selected items by their id. Replaces the current selection
  433. * Unknown id's are silently ignored.
  434. * @param {Array} [ids] An array with zero or more id's of the items to be
  435. * selected. If ids is an empty array, all items will be
  436. * unselected.
  437. */
  438. Timeline.prototype.setSelection = function(ids) {
  439. this.itemSet && this.itemSet.setSelection(ids);
  440. };
  441. /**
  442. * Get the selected items by their id
  443. * @return {Array} ids The ids of the selected items
  444. */
  445. Timeline.prototype.getSelection = function() {
  446. return this.itemSet && this.itemSet.getSelection() || [];
  447. };
  448. /**
  449. * Set the visible window. Both parameters are optional, you can change only
  450. * start or only end. Syntax:
  451. *
  452. * TimeLine.setWindow(start, end)
  453. * TimeLine.setWindow(range)
  454. *
  455. * Where start and end can be a Date, number, or string, and range is an
  456. * object with properties start and end.
  457. *
  458. * @param {Date | Number | String | Object} [start] Start date of visible window
  459. * @param {Date | Number | String} [end] End date of visible window
  460. */
  461. Timeline.prototype.setWindow = function(start, end) {
  462. if (arguments.length == 1) {
  463. var range = arguments[0];
  464. this.range.setRange(range.start, range.end);
  465. }
  466. else {
  467. this.range.setRange(start, end);
  468. }
  469. };
  470. /**
  471. * Get the visible window
  472. * @return {{start: Date, end: Date}} Visible range
  473. */
  474. Timeline.prototype.getWindow = function() {
  475. var range = this.range.getRange();
  476. return {
  477. start: new Date(range.start),
  478. end: new Date(range.end)
  479. };
  480. };
  481. /**
  482. * Force a redraw of the Timeline. Can be useful to manually redraw when
  483. * option autoResize=false
  484. */
  485. Timeline.prototype.redraw = function() {
  486. var resized = false,
  487. options = this.options,
  488. props = this.props,
  489. dom = this.dom;
  490. if (!dom) return; // when destroyed
  491. // update class names
  492. dom.root.className = 'vis timeline root ' + options.orientation;
  493. // update root width and height options
  494. dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
  495. dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
  496. dom.root.style.width = util.option.asSize(options.width, '');
  497. // calculate border widths
  498. props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
  499. props.border.right = props.border.left;
  500. props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
  501. props.border.bottom = props.border.top;
  502. var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
  503. var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
  504. // calculate the heights. If any of the side panels is empty, we set the height to
  505. // minus the border width, such that the border will be invisible
  506. props.center.height = dom.center.offsetHeight;
  507. props.left.height = dom.left.offsetHeight;
  508. props.right.height = dom.right.offsetHeight;
  509. props.top.height = dom.top.clientHeight || -props.border.top;
  510. props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
  511. // TODO: compensate borders when any of the panels is empty.
  512. // apply auto height
  513. // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
  514. var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
  515. var autoHeight = props.top.height + contentHeight + props.bottom.height +
  516. borderRootHeight + props.border.top + props.border.bottom;
  517. dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
  518. // calculate heights of the content panels
  519. props.root.height = dom.root.offsetHeight;
  520. props.background.height = props.root.height - borderRootHeight;
  521. var containerHeight = props.root.height - props.top.height - props.bottom.height -
  522. borderRootHeight;
  523. props.centerContainer.height = containerHeight;
  524. props.leftContainer.height = containerHeight;
  525. props.rightContainer.height = props.leftContainer.height;
  526. // calculate the widths of the panels
  527. props.root.width = dom.root.offsetWidth;
  528. props.background.width = props.root.width - borderRootWidth;
  529. props.left.width = dom.leftContainer.clientWidth || -props.border.left;
  530. props.leftContainer.width = props.left.width;
  531. props.right.width = dom.rightContainer.clientWidth || -props.border.right;
  532. props.rightContainer.width = props.right.width;
  533. var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
  534. props.center.width = centerWidth;
  535. props.centerContainer.width = centerWidth;
  536. props.top.width = centerWidth;
  537. props.bottom.width = centerWidth;
  538. // resize the panels
  539. dom.background.style.height = props.background.height + 'px';
  540. dom.backgroundVertical.style.height = props.background.height + 'px';
  541. dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
  542. dom.centerContainer.style.height = props.centerContainer.height + 'px';
  543. dom.leftContainer.style.height = props.leftContainer.height + 'px';
  544. dom.rightContainer.style.height = props.rightContainer.height + 'px';
  545. dom.background.style.width = props.background.width + 'px';
  546. dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
  547. dom.backgroundHorizontal.style.width = props.background.width + 'px';
  548. dom.centerContainer.style.width = props.center.width + 'px';
  549. dom.top.style.width = props.top.width + 'px';
  550. dom.bottom.style.width = props.bottom.width + 'px';
  551. // reposition the panels
  552. dom.background.style.left = '0';
  553. dom.background.style.top = '0';
  554. dom.backgroundVertical.style.left = props.left.width + 'px';
  555. dom.backgroundVertical.style.top = '0';
  556. dom.backgroundHorizontal.style.left = '0';
  557. dom.backgroundHorizontal.style.top = props.top.height + 'px';
  558. dom.centerContainer.style.left = props.left.width + 'px';
  559. dom.centerContainer.style.top = props.top.height + 'px';
  560. dom.leftContainer.style.left = '0';
  561. dom.leftContainer.style.top = props.top.height + 'px';
  562. dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
  563. dom.rightContainer.style.top = props.top.height + 'px';
  564. dom.top.style.left = props.left.width + 'px';
  565. dom.top.style.top = '0';
  566. dom.bottom.style.left = props.left.width + 'px';
  567. dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
  568. // update the scrollTop, feasible range for the offset can be changed
  569. // when the height of the Timeline or of the contents of the center changed
  570. this._updateScrollTop();
  571. // reposition the scrollable contents
  572. var offset = this.props.scrollTop;
  573. if (options.orientation == 'bottom') {
  574. offset += Math.max(this.props.centerContainer.height - this.props.center.height -
  575. this.props.border.top - this.props.border.bottom, 0);
  576. }
  577. dom.center.style.left = '0';
  578. dom.center.style.top = offset + 'px';
  579. dom.left.style.left = '0';
  580. dom.left.style.top = offset + 'px';
  581. dom.right.style.left = '0';
  582. dom.right.style.top = offset + 'px';
  583. // show shadows when vertical scrolling is available
  584. var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
  585. var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
  586. dom.shadowTop.style.visibility = visibilityTop;
  587. dom.shadowBottom.style.visibility = visibilityBottom;
  588. dom.shadowTopLeft.style.visibility = visibilityTop;
  589. dom.shadowBottomLeft.style.visibility = visibilityBottom;
  590. dom.shadowTopRight.style.visibility = visibilityTop;
  591. dom.shadowBottomRight.style.visibility = visibilityBottom;
  592. // redraw all components
  593. this.components.forEach(function (component) {
  594. resized = component.redraw() || resized;
  595. });
  596. if (resized) {
  597. // keep repainting until all sizes are settled
  598. this.redraw();
  599. }
  600. };
  601. // TODO: deprecated since version 1.1.0, remove some day
  602. Timeline.prototype.repaint = function () {
  603. throw new Error('Function repaint is deprecated. Use redraw instead.');
  604. };
  605. /**
  606. * Convert a position on screen (pixels) to a datetime
  607. * @param {int} x Position on the screen in pixels
  608. * @return {Date} time The datetime the corresponds with given position x
  609. * @private
  610. */
  611. // TODO: move this function to Range
  612. Timeline.prototype._toTime = function(x) {
  613. var conversion = this.range.conversion(this.props.center.width);
  614. return new Date(x / conversion.scale + conversion.offset);
  615. };
  616. /**
  617. * Convert a position on the global screen (pixels) to a datetime
  618. * @param {int} x Position on the screen in pixels
  619. * @return {Date} time The datetime the corresponds with given position x
  620. * @private
  621. */
  622. // TODO: move this function to Range
  623. Timeline.prototype._toGlobalTime = function(x) {
  624. var conversion = this.range.conversion(this.props.root.width);
  625. return new Date(x / conversion.scale + conversion.offset);
  626. };
  627. /**
  628. * Convert a datetime (Date object) into a position on the screen
  629. * @param {Date} time A date
  630. * @return {int} x The position on the screen in pixels which corresponds
  631. * with the given date.
  632. * @private
  633. */
  634. // TODO: move this function to Range
  635. Timeline.prototype._toScreen = function(time) {
  636. var conversion = this.range.conversion(this.props.center.width);
  637. return (time.valueOf() - conversion.offset) * conversion.scale;
  638. };
  639. /**
  640. * Convert a datetime (Date object) into a position on the root
  641. * This is used to get the pixel density estimate for the screen, not the center panel
  642. * @param {Date} time A date
  643. * @return {int} x The position on root in pixels which corresponds
  644. * with the given date.
  645. * @private
  646. */
  647. // TODO: move this function to Range
  648. Timeline.prototype._toGlobalScreen = function(time) {
  649. var conversion = this.range.conversion(this.props.root.width);
  650. return (time.valueOf() - conversion.offset) * conversion.scale;
  651. };
  652. /**
  653. * Initialize watching when option autoResize is true
  654. * @private
  655. */
  656. Timeline.prototype._initAutoResize = function () {
  657. if (this.options.autoResize == true) {
  658. this._startAutoResize();
  659. }
  660. else {
  661. this._stopAutoResize();
  662. }
  663. };
  664. /**
  665. * Watch for changes in the size of the container. On resize, the Panel will
  666. * automatically redraw itself.
  667. * @private
  668. */
  669. Timeline.prototype._startAutoResize = function () {
  670. var me = this;
  671. this._stopAutoResize();
  672. this._onResize = function() {
  673. if (me.options.autoResize != true) {
  674. // stop watching when the option autoResize is changed to false
  675. me._stopAutoResize();
  676. return;
  677. }
  678. if (me.dom.root) {
  679. // check whether the frame is resized
  680. if ((me.dom.root.clientWidth != me.props.lastWidth) ||
  681. (me.dom.root.clientHeight != me.props.lastHeight)) {
  682. me.props.lastWidth = me.dom.root.clientWidth;
  683. me.props.lastHeight = me.dom.root.clientHeight;
  684. me.emit('change');
  685. }
  686. }
  687. };
  688. // add event listener to window resize
  689. util.addEventListener(window, 'resize', this._onResize);
  690. this.watchTimer = setInterval(this._onResize, 1000);
  691. };
  692. /**
  693. * Stop watching for a resize of the frame.
  694. * @private
  695. */
  696. Timeline.prototype._stopAutoResize = function () {
  697. if (this.watchTimer) {
  698. clearInterval(this.watchTimer);
  699. this.watchTimer = undefined;
  700. }
  701. // remove event listener on window.resize
  702. util.removeEventListener(window, 'resize', this._onResize);
  703. this._onResize = null;
  704. };
  705. /**
  706. * Start moving the timeline vertically
  707. * @param {Event} event
  708. * @private
  709. */
  710. Timeline.prototype._onTouch = function (event) {
  711. this.touch.allowDragging = true;
  712. };
  713. /**
  714. * Start moving the timeline vertically
  715. * @param {Event} event
  716. * @private
  717. */
  718. Timeline.prototype._onPinch = function (event) {
  719. this.touch.allowDragging = false;
  720. };
  721. /**
  722. * Start moving the timeline vertically
  723. * @param {Event} event
  724. * @private
  725. */
  726. Timeline.prototype._onDragStart = function (event) {
  727. this.touch.initialScrollTop = this.props.scrollTop;
  728. };
  729. /**
  730. * Move the timeline vertically
  731. * @param {Event} event
  732. * @private
  733. */
  734. Timeline.prototype._onDrag = function (event) {
  735. // refuse to drag when we where pinching to prevent the timeline make a jump
  736. // when releasing the fingers in opposite order from the touch screen
  737. if (!this.touch.allowDragging) return;
  738. var delta = event.gesture.deltaY;
  739. var oldScrollTop = this._getScrollTop();
  740. var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
  741. if (newScrollTop != oldScrollTop) {
  742. this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
  743. }
  744. };
  745. /**
  746. * Apply a scrollTop
  747. * @param {Number} scrollTop
  748. * @returns {Number} scrollTop Returns the applied scrollTop
  749. * @private
  750. */
  751. Timeline.prototype._setScrollTop = function (scrollTop) {
  752. this.props.scrollTop = scrollTop;
  753. this._updateScrollTop();
  754. return this.props.scrollTop;
  755. };
  756. /**
  757. * Update the current scrollTop when the height of the containers has been changed
  758. * @returns {Number} scrollTop Returns the applied scrollTop
  759. * @private
  760. */
  761. Timeline.prototype._updateScrollTop = function () {
  762. // recalculate the scrollTopMin
  763. var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
  764. if (scrollTopMin != this.props.scrollTopMin) {
  765. // in case of bottom orientation, change the scrollTop such that the contents
  766. // do not move relative to the time axis at the bottom
  767. if (this.options.orientation == 'bottom') {
  768. this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
  769. }
  770. this.props.scrollTopMin = scrollTopMin;
  771. }
  772. // limit the scrollTop to the feasible scroll range
  773. if (this.props.scrollTop > 0) this.props.scrollTop = 0;
  774. if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
  775. return this.props.scrollTop;
  776. };
  777. /**
  778. * Get the current scrollTop
  779. * @returns {number} scrollTop
  780. * @private
  781. */
  782. Timeline.prototype._getScrollTop = function () {
  783. return this.props.scrollTop;
  784. };
  785. module.exports = Timeline;