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.

889 lines
28 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
  1. var util = require('../util');
  2. var hammerUtil = require('../hammerUtil');
  3. var moment = require('../module/moment');
  4. var Component = require('./component/Component');
  5. var DateUtil = require('./DateUtil');
  6. /**
  7. * @constructor Range
  8. * A Range controls a numeric range with a start and end value.
  9. * The Range adjusts the range based on mouse events or programmatic changes,
  10. * and triggers events when the range is changing or has been changed.
  11. * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
  12. * @param {Object} [options] See description at Range.setOptions
  13. */
  14. function Range(body, options) {
  15. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  16. var start = now.clone().add(-3, 'days').valueOf();
  17. var end = now.clone().add(3, 'days').valueOf();
  18. if(options === undefined) {
  19. this.start = start;
  20. this.end = end;
  21. } else {
  22. this.start = options.start || start
  23. this.end = options.end || end
  24. }
  25. this.rolling = false;
  26. this.body = body;
  27. this.deltaDifference = 0;
  28. this.scaleOffset = 0;
  29. this.startToFront = false;
  30. this.endToFront = true;
  31. // default options
  32. this.defaultOptions = {
  33. rtl: false,
  34. start: null,
  35. end: null,
  36. moment: moment,
  37. direction: 'horizontal', // 'horizontal' or 'vertical'
  38. moveable: true,
  39. zoomable: true,
  40. min: null,
  41. max: null,
  42. zoomMin: 10, // milliseconds
  43. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  44. rollingMode: {
  45. follow: false,
  46. offset: 0.5
  47. }
  48. };
  49. this.options = util.extend({}, this.defaultOptions);
  50. this.props = {
  51. touch: {}
  52. };
  53. this.animationTimer = null;
  54. // drag listeners for dragging
  55. this.body.emitter.on('panstart', this._onDragStart.bind(this));
  56. this.body.emitter.on('panmove', this._onDrag.bind(this));
  57. this.body.emitter.on('panend', this._onDragEnd.bind(this));
  58. // mouse wheel for zooming
  59. this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
  60. // pinch to zoom
  61. this.body.emitter.on('touch', this._onTouch.bind(this));
  62. this.body.emitter.on('pinch', this._onPinch.bind(this));
  63. // on click of rolling mode button
  64. this.body.dom.rollingModeBtn.addEventListener('click', this.startRolling.bind(this));
  65. this.setOptions(options);
  66. }
  67. Range.prototype = new Component();
  68. /**
  69. * Set options for the range controller
  70. * @param {Object} options Available options:
  71. * {Number | Date | String} start Start date for the range
  72. * {Number | Date | String} end End date for the range
  73. * {Number} min Minimum value for start
  74. * {Number} max Maximum value for end
  75. * {Number} zoomMin Set a minimum value for
  76. * (end - start).
  77. * {Number} zoomMax Set a maximum value for
  78. * (end - start).
  79. * {Boolean} moveable Enable moving of the range
  80. * by dragging. True by default
  81. * {Boolean} zoomable Enable zooming of the range
  82. * by pinching/scrolling. True by default
  83. */
  84. Range.prototype.setOptions = function (options) {
  85. if (options) {
  86. // copy the options that we know
  87. var fields = [
  88. 'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable',
  89. 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'showCurrentTime', 'rollingMode', 'horizontalScroll'
  90. ];
  91. util.selectiveExtend(fields, this.options, options);
  92. if (options.rollingMode && options.rollingMode.follow) {
  93. this.startRolling();
  94. }
  95. if ('start' in options || 'end' in options) {
  96. // apply a new range. both start and end are optional
  97. this.setRange(options.start, options.end);
  98. }
  99. }
  100. };
  101. /**
  102. * Test whether direction has a valid value
  103. * @param {String} direction 'horizontal' or 'vertical'
  104. */
  105. function validateDirection (direction) {
  106. if (direction != 'horizontal' && direction != 'vertical') {
  107. throw new TypeError('Unknown direction "' + direction + '". ' +
  108. 'Choose "horizontal" or "vertical".');
  109. }
  110. }
  111. /**
  112. * Start auto refreshing the current time bar
  113. */
  114. Range.prototype.startRolling = function() {
  115. var me = this;
  116. function update () {
  117. me.stopRolling();
  118. me.rolling = true;
  119. var interval = me.end - me.start;
  120. var t = util.convert(new Date(), 'Date').valueOf();
  121. var start = t - interval * (me.options.rollingMode.offset);
  122. var end = t + interval * (1 - me.options.rollingMode.offset);
  123. var animation = (me.options && me.options.animation !== undefined) ? me.options.animation : true;
  124. var options = {
  125. animation: false
  126. };
  127. me.setRange(start, end, options);
  128. // determine interval to refresh
  129. var scale = me.conversion(me.body.domProps.center.width).scale;
  130. var interval = 1 / scale / 10;
  131. if (interval < 30) interval = 30;
  132. if (interval > 1000) interval = 1000;
  133. me.body.dom.rollingModeBtn.style.visibility = "hidden";
  134. // start a renderTimer to adjust for the new time
  135. me.currentTimeTimer = setTimeout(update, interval);
  136. }
  137. update();
  138. };
  139. /**
  140. * Stop auto refreshing the current time bar
  141. */
  142. Range.prototype.stopRolling = function() {
  143. if (this.currentTimeTimer !== undefined) {
  144. clearTimeout(this.currentTimeTimer);
  145. this.rolling = false;
  146. this.body.dom.rollingModeBtn.style.visibility = "visible";
  147. }
  148. };
  149. /**
  150. * Set a new start and end range
  151. * @param {Date | Number | String} [start]
  152. * @param {Date | Number | String} [end]
  153. * @param {Object} options Available options:
  154. * {Boolean | {duration: number, easingFunction: string}} [animation=false]
  155. * If true, the range is animated
  156. * smoothly to the new window. An object can be
  157. * provided to specify duration and easing function.
  158. * Default duration is 500 ms, and default easing
  159. * function is 'easeInOutQuad'.
  160. * {Boolean} [byUser=false]
  161. * {Event} event Mouse event
  162. * {Function} a callback funtion to be executed at the end of this function
  163. *
  164. */
  165. Range.prototype.setRange = function(start, end, options, callback) {
  166. if (!options) {
  167. options = {};
  168. }
  169. if (options.byUser !== true) {
  170. options.byUser = false;
  171. }
  172. var me = this;
  173. var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null;
  174. var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null;
  175. this._cancelAnimation();
  176. if (options.animation) { // true or an Object
  177. var initStart = this.start;
  178. var initEnd = this.end;
  179. var duration = (typeof options.animation === 'object' && 'duration' in options.animation) ? options.animation.duration : 500;
  180. var easingName = (typeof options.animation === 'object' && 'easingFunction' in options.animation) ? options.animation.easingFunction : 'easeInOutQuad';
  181. var easingFunction = util.easingFunctions[easingName];
  182. if (!easingFunction) {
  183. throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' +
  184. 'Choose from: ' + Object.keys(util.easingFunctions).join(', '));
  185. }
  186. var initTime = new Date().valueOf();
  187. var anyChanged = false;
  188. var next = function () {
  189. if (!me.props.touch.dragging) {
  190. var now = new Date().valueOf();
  191. var time = now - initTime;
  192. var ease = easingFunction(time / duration);
  193. var done = time > duration;
  194. var s = (done || finalStart === null) ? finalStart : initStart + (finalStart - initStart) * ease;
  195. var e = (done || finalEnd === null) ? finalEnd : initEnd + (finalEnd - initEnd) * ease;
  196. changed = me._applyRange(s, e);
  197. DateUtil.updateHiddenDates(me.options.moment, me.body, me.options.hiddenDates);
  198. anyChanged = anyChanged || changed;
  199. var params = {
  200. start: new Date(me.start),
  201. end: new Date(me.end),
  202. byUser: options.byUser,
  203. event: options.event
  204. }
  205. if (changed) {
  206. me.body.emitter.emit('rangechange', params);
  207. }
  208. if (done) {
  209. if (anyChanged) {
  210. me.body.emitter.emit('rangechanged', params);
  211. if (callback) { return callback() }
  212. }
  213. }
  214. else {
  215. // animate with as high as possible frame rate, leave 20 ms in between
  216. // each to prevent the browser from blocking
  217. me.animationTimer = setTimeout(next, 20);
  218. }
  219. }
  220. };
  221. return next();
  222. }
  223. else {
  224. var changed = this._applyRange(finalStart, finalEnd);
  225. DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);
  226. if (changed) {
  227. var params = {
  228. start: new Date(this.start),
  229. end: new Date(this.end),
  230. byUser: options.byUser,
  231. event: options.event
  232. };
  233. this.body.emitter.emit('rangechange', params);
  234. clearTimeout( me.timeoutID );
  235. me.timeoutID = setTimeout( function () {
  236. me.body.emitter.emit('rangechanged', params);
  237. }, 200 );
  238. if (callback) { return callback() }
  239. }
  240. }
  241. };
  242. /**
  243. * Get the number of milliseconds per pixel.
  244. */
  245. Range.prototype.getMillisecondsPerPixel = function() {
  246. return (this.end - this.start) / this.body.dom.center.clientWidth;
  247. }
  248. /**
  249. * Stop an animation
  250. * @private
  251. */
  252. Range.prototype._cancelAnimation = function () {
  253. if (this.animationTimer) {
  254. clearTimeout(this.animationTimer);
  255. this.animationTimer = null;
  256. }
  257. };
  258. /**
  259. * Set a new start and end range. This method is the same as setRange, but
  260. * does not trigger a range change and range changed event, and it returns
  261. * true when the range is changed
  262. * @param {Number} [start]
  263. * @param {Number} [end]
  264. * @return {Boolean} changed
  265. * @private
  266. */
  267. Range.prototype._applyRange = function(start, end) {
  268. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  269. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  270. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  271. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  272. diff;
  273. // check for valid number
  274. if (isNaN(newStart) || newStart === null) {
  275. throw new Error('Invalid start "' + start + '"');
  276. }
  277. if (isNaN(newEnd) || newEnd === null) {
  278. throw new Error('Invalid end "' + end + '"');
  279. }
  280. // prevent end < start
  281. if (newEnd < newStart) {
  282. newEnd = newStart;
  283. }
  284. // prevent start < min
  285. if (min !== null) {
  286. if (newStart < min) {
  287. diff = (min - newStart);
  288. newStart += diff;
  289. newEnd += diff;
  290. // prevent end > max
  291. if (max != null) {
  292. if (newEnd > max) {
  293. newEnd = max;
  294. }
  295. }
  296. }
  297. }
  298. // prevent end > max
  299. if (max !== null) {
  300. if (newEnd > max) {
  301. diff = (newEnd - max);
  302. newStart -= diff;
  303. newEnd -= diff;
  304. // prevent start < min
  305. if (min != null) {
  306. if (newStart < min) {
  307. newStart = min;
  308. }
  309. }
  310. }
  311. }
  312. // prevent (end-start) < zoomMin
  313. if (this.options.zoomMin !== null) {
  314. var zoomMin = parseFloat(this.options.zoomMin);
  315. if (zoomMin < 0) {
  316. zoomMin = 0;
  317. }
  318. if ((newEnd - newStart) < zoomMin) {
  319. // compensate for a scale of 0.5 ms
  320. var compensation = 0.5;
  321. if ((this.end - this.start) === zoomMin && newStart >= this.start - compensation && newEnd <= this.end) {
  322. // ignore this action, we are already zoomed to the minimum
  323. newStart = this.start;
  324. newEnd = this.end;
  325. }
  326. else {
  327. // zoom to the minimum
  328. diff = (zoomMin - (newEnd - newStart));
  329. newStart -= diff / 2;
  330. newEnd += diff / 2;
  331. }
  332. }
  333. }
  334. // prevent (end-start) > zoomMax
  335. if (this.options.zoomMax !== null) {
  336. var zoomMax = parseFloat(this.options.zoomMax);
  337. if (zoomMax < 0) {
  338. zoomMax = 0;
  339. }
  340. if ((newEnd - newStart) > zoomMax) {
  341. if ((this.end - this.start) === zoomMax && newStart < this.start && newEnd > this.end) {
  342. // ignore this action, we are already zoomed to the maximum
  343. newStart = this.start;
  344. newEnd = this.end;
  345. }
  346. else {
  347. // zoom to the maximum
  348. diff = ((newEnd - newStart) - zoomMax);
  349. newStart += diff / 2;
  350. newEnd -= diff / 2;
  351. }
  352. }
  353. }
  354. var changed = (this.start != newStart || this.end != newEnd);
  355. // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range)
  356. if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&
  357. !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {
  358. this.body.emitter.emit('checkRangedItems');
  359. }
  360. this.start = newStart;
  361. this.end = newEnd;
  362. return changed;
  363. };
  364. /**
  365. * Retrieve the current range.
  366. * @return {Object} An object with start and end properties
  367. */
  368. Range.prototype.getRange = function() {
  369. return {
  370. start: this.start,
  371. end: this.end
  372. };
  373. };
  374. /**
  375. * Calculate the conversion offset and scale for current range, based on
  376. * the provided width
  377. * @param {Number} width
  378. * @returns {{offset: number, scale: number}} conversion
  379. */
  380. Range.prototype.conversion = function (width, totalHidden) {
  381. return Range.conversion(this.start, this.end, width, totalHidden);
  382. };
  383. /**
  384. * Static method to calculate the conversion offset and scale for a range,
  385. * based on the provided start, end, and width
  386. * @param {Number} start
  387. * @param {Number} end
  388. * @param {Number} width
  389. * @returns {{offset: number, scale: number}} conversion
  390. */
  391. Range.conversion = function (start, end, width, totalHidden) {
  392. if (totalHidden === undefined) {
  393. totalHidden = 0;
  394. }
  395. if (width != 0 && (end - start != 0)) {
  396. return {
  397. offset: start,
  398. scale: width / (end - start - totalHidden)
  399. }
  400. }
  401. else {
  402. return {
  403. offset: 0,
  404. scale: 1
  405. };
  406. }
  407. };
  408. /**
  409. * Start dragging horizontally or vertically
  410. * @param {Event} event
  411. * @private
  412. */
  413. Range.prototype._onDragStart = function(event) {
  414. this.deltaDifference = 0;
  415. this.previousDelta = 0;
  416. // only allow dragging when configured as movable
  417. if (!this.options.moveable) return;
  418. // only start dragging when the mouse is inside the current range
  419. if (!this._isInsideRange(event)) return;
  420. // refuse to drag when we where pinching to prevent the timeline make a jump
  421. // when releasing the fingers in opposite order from the touch screen
  422. if (!this.props.touch.allowDragging) return;
  423. this.stopRolling();
  424. this.props.touch.start = this.start;
  425. this.props.touch.end = this.end;
  426. this.props.touch.dragging = true;
  427. if (this.body.dom.root) {
  428. this.body.dom.root.style.cursor = 'move';
  429. }
  430. };
  431. /**
  432. * Perform dragging operation
  433. * @param {Event} event
  434. * @private
  435. */
  436. Range.prototype._onDrag = function (event) {
  437. if (!event) return
  438. if (!this.props.touch.dragging) return;
  439. // only allow dragging when configured as movable
  440. if (!this.options.moveable) return;
  441. // TODO: this may be redundant in hammerjs2
  442. // refuse to drag when we where pinching to prevent the timeline make a jump
  443. // when releasing the fingers in opposite order from the touch screen
  444. if (!this.props.touch.allowDragging) return;
  445. var direction = this.options.direction;
  446. validateDirection(direction);
  447. var delta = (direction == 'horizontal') ? event.deltaX : event.deltaY;
  448. delta -= this.deltaDifference;
  449. var interval = (this.props.touch.end - this.props.touch.start);
  450. // normalize dragging speed if cutout is in between.
  451. var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  452. interval -= duration;
  453. var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
  454. if (this.options.rtl) {
  455. var diffRange = delta / width * interval;
  456. } else {
  457. var diffRange = -delta / width * interval;
  458. }
  459. var newStart = this.props.touch.start + diffRange;
  460. var newEnd = this.props.touch.end + diffRange;
  461. // snapping times away from hidden zones
  462. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true);
  463. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true);
  464. if (safeStart != newStart || safeEnd != newEnd) {
  465. this.deltaDifference += delta;
  466. this.props.touch.start = safeStart;
  467. this.props.touch.end = safeEnd;
  468. this._onDrag(event);
  469. return;
  470. }
  471. this.previousDelta = delta;
  472. this._applyRange(newStart, newEnd);
  473. var startDate = new Date(this.start);
  474. var endDate = new Date(this.end);
  475. // fire a rangechange event
  476. this.body.emitter.emit('rangechange', {
  477. start: startDate,
  478. end: endDate,
  479. byUser: true,
  480. event: event
  481. });
  482. // fire a panmove event
  483. this.body.emitter.emit('panmove');
  484. };
  485. /**
  486. * Stop dragging operation
  487. * @param {event} event
  488. * @private
  489. */
  490. Range.prototype._onDragEnd = function (event) {
  491. if (!this.props.touch.dragging) return;
  492. // only allow dragging when configured as movable
  493. if (!this.options.moveable) return;
  494. // TODO: this may be redundant in hammerjs2
  495. // refuse to drag when we where pinching to prevent the timeline make a jump
  496. // when releasing the fingers in opposite order from the touch screen
  497. if (!this.props.touch.allowDragging) return;
  498. this.props.touch.dragging = false;
  499. if (this.body.dom.root) {
  500. this.body.dom.root.style.cursor = 'auto';
  501. }
  502. // fire a rangechanged event
  503. this.body.emitter.emit('rangechanged', {
  504. start: new Date(this.start),
  505. end: new Date(this.end),
  506. byUser: true,
  507. event: event
  508. });
  509. };
  510. /**
  511. * Event handler for mouse wheel event, used to zoom
  512. * Code from http://adomas.org/javascript-mouse-wheel/
  513. * @param {Event} event
  514. * @private
  515. */
  516. Range.prototype._onMouseWheel = function(event) {
  517. // retrieve delta
  518. var delta = 0;
  519. if (event.wheelDelta) { /* IE/Opera. */
  520. delta = event.wheelDelta / 120;
  521. } else if (event.detail) { /* Mozilla case. */
  522. // In Mozilla, sign of delta is different than in IE.
  523. // Also, delta is multiple of 3.
  524. delta = -event.detail / 3;
  525. }
  526. // don't allow zoom when the according key is pressed and the zoomKey option or not zoomable but movable
  527. if ((this.options.zoomKey && !event[this.options.zoomKey] && this.options.zoomable)
  528. || (!this.options.zoomable && this.options.moveable)) {
  529. if (this.options.horizontalScroll) {
  530. // Prevent default actions caused by mouse wheel
  531. // (else the page and timeline both scroll)
  532. event.preventDefault();
  533. // calculate a single scroll jump relative to the range scale
  534. var diff = delta * (this.end - this.start) / 20;
  535. // calculate new start and end
  536. var newStart = this.start - diff;
  537. var newEnd = this.end - diff;
  538. var options = {
  539. animation: false,
  540. byUser: true,
  541. event: event
  542. }
  543. this.setRange(newStart, newEnd, options);
  544. }
  545. return;
  546. }
  547. // only allow zooming when configured as zoomable and moveable
  548. if (!(this.options.zoomable && this.options.moveable)) return;
  549. // only zoom when the mouse is inside the current range
  550. if (!this._isInsideRange(event)) return;
  551. // If delta is nonzero, handle it.
  552. // Basically, delta is now positive if wheel was scrolled up,
  553. // and negative, if wheel was scrolled down.
  554. if (delta) {
  555. // perform the zoom action. Delta is normally 1 or -1
  556. // adjust a negative delta such that zooming in with delta 0.1
  557. // equals zooming out with a delta -0.1
  558. var scale;
  559. if (delta < 0) {
  560. scale = 1 - (delta / 5);
  561. }
  562. else {
  563. scale = 1 / (1 + (delta / 5)) ;
  564. }
  565. // calculate center, the date to zoom around
  566. var pointerDate
  567. if (this.rolling) {
  568. pointerDate = this.start + ((this.end - this.start) * this.options.rollingMode.offset);
  569. } else {
  570. var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center);
  571. pointerDate = this._pointerToDate(pointer);
  572. }
  573. this.zoom(scale, pointerDate, delta, event);
  574. // Prevent default actions caused by mouse wheel
  575. // (else the page and timeline both scroll)
  576. event.preventDefault();
  577. }
  578. };
  579. /**
  580. * Start of a touch gesture
  581. * @private
  582. */
  583. Range.prototype._onTouch = function (event) {
  584. this.props.touch.start = this.start;
  585. this.props.touch.end = this.end;
  586. this.props.touch.allowDragging = true;
  587. this.props.touch.center = null;
  588. this.scaleOffset = 0;
  589. this.deltaDifference = 0;
  590. };
  591. /**
  592. * Handle pinch event
  593. * @param {Event} event
  594. * @private
  595. */
  596. Range.prototype._onPinch = function (event) {
  597. // only allow zooming when configured as zoomable and moveable
  598. if (!(this.options.zoomable && this.options.moveable)) return;
  599. this.props.touch.allowDragging = false;
  600. if (!this.props.touch.center) {
  601. this.props.touch.center = this.getPointer(event.center, this.body.dom.center);
  602. }
  603. this.stopRolling();
  604. var scale = 1 / (event.scale + this.scaleOffset);
  605. var centerDate = this._pointerToDate(this.props.touch.center);
  606. var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  607. var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this, centerDate);
  608. var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
  609. // calculate new start and end
  610. var newStart = (centerDate - hiddenDurationBefore) + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale;
  611. var newEnd = (centerDate + hiddenDurationAfter) + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale;
  612. // snapping times away from hidden zones
  613. this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times
  614. this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times
  615. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true);
  616. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true);
  617. if (safeStart != newStart || safeEnd != newEnd) {
  618. this.props.touch.start = safeStart;
  619. this.props.touch.end = safeEnd;
  620. this.scaleOffset = 1 - event.scale;
  621. newStart = safeStart;
  622. newEnd = safeEnd;
  623. }
  624. var options = {
  625. animation: false,
  626. byUser: true,
  627. event: event
  628. }
  629. this.setRange(newStart, newEnd, options);
  630. this.startToFront = false; // revert to default
  631. this.endToFront = true; // revert to default
  632. };
  633. /**
  634. * Test whether the mouse from a mouse event is inside the visible window,
  635. * between the current start and end date
  636. * @param {Object} event
  637. * @return {boolean} Returns true when inside the visible window
  638. * @private
  639. */
  640. Range.prototype._isInsideRange = function(event) {
  641. // calculate the time where the mouse is, check whether inside
  642. // and no scroll action should happen.
  643. var clientX = event.center ? event.center.x : event.clientX;
  644. if (this.options.rtl) {
  645. var x = clientX - util.getAbsoluteLeft(this.body.dom.centerContainer);
  646. } else {
  647. var x = util.getAbsoluteRight(this.body.dom.centerContainer) - clientX;
  648. }
  649. var time = this.body.util.toTime(x);
  650. return time >= this.start && time <= this.end;
  651. };
  652. /**
  653. * Helper function to calculate the center date for zooming
  654. * @param {{x: Number, y: Number}} pointer
  655. * @return {number} date
  656. * @private
  657. */
  658. Range.prototype._pointerToDate = function (pointer) {
  659. var conversion;
  660. var direction = this.options.direction;
  661. validateDirection(direction);
  662. if (direction == 'horizontal') {
  663. return this.body.util.toTime(pointer.x).valueOf();
  664. }
  665. else {
  666. var height = this.body.domProps.center.height;
  667. conversion = this.conversion(height);
  668. return pointer.y / conversion.scale + conversion.offset;
  669. }
  670. };
  671. /**
  672. * Get the pointer location relative to the location of the dom element
  673. * @param {{x: Number, y: Number}} touch
  674. * @param {Element} element HTML DOM element
  675. * @return {{x: Number, y: Number}} pointer
  676. * @private
  677. */
  678. Range.prototype.getPointer = function (touch, element) {
  679. if (this.options.rtl) {
  680. return {
  681. x: util.getAbsoluteRight(element) - touch.x,
  682. y: touch.y - util.getAbsoluteTop(element)
  683. };
  684. } else {
  685. return {
  686. x: touch.x - util.getAbsoluteLeft(element),
  687. y: touch.y - util.getAbsoluteTop(element)
  688. };
  689. }
  690. }
  691. /**
  692. * Zoom the range the given scale in or out. Start and end date will
  693. * be adjusted, and the timeline will be redrawn. You can optionally give a
  694. * date around which to zoom.
  695. * For example, try scale = 0.9 or 1.1
  696. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  697. * values below 1 will zoom in.
  698. * @param {Number} [center] Value representing a date around which will
  699. * be zoomed.
  700. */
  701. Range.prototype.zoom = function(scale, center, delta, event) {
  702. // if centerDate is not provided, take it half between start Date and end Date
  703. if (center == null) {
  704. center = (this.start + this.end) / 2;
  705. }
  706. var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  707. var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this, center);
  708. var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
  709. // calculate new start and end
  710. var newStart = (center-hiddenDurationBefore) + (this.start - (center-hiddenDurationBefore)) * scale;
  711. var newEnd = (center+hiddenDurationAfter) + (this.end - (center+hiddenDurationAfter)) * scale;
  712. // snapping times away from hidden zones
  713. this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  714. this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  715. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true);
  716. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true);
  717. if (safeStart != newStart || safeEnd != newEnd) {
  718. newStart = safeStart;
  719. newEnd = safeEnd;
  720. }
  721. var options = {
  722. animation: false,
  723. byUser: true,
  724. event: event
  725. }
  726. this.setRange(newStart, newEnd, options);
  727. this.startToFront = false; // revert to default
  728. this.endToFront = true; // revert to default
  729. };
  730. /**
  731. * Move the range with a given delta to the left or right. Start and end
  732. * value will be adjusted. For example, try delta = 0.1 or -0.1
  733. * @param {Number} delta Moving amount. Positive value will move right,
  734. * negative value will move left
  735. */
  736. Range.prototype.move = function(delta) {
  737. // zoom start Date and end Date relative to the centerDate
  738. var diff = (this.end - this.start);
  739. // apply new values
  740. var newStart = this.start + diff * delta;
  741. var newEnd = this.end + diff * delta;
  742. // TODO: reckon with min and max range
  743. this.start = newStart;
  744. this.end = newEnd;
  745. };
  746. /**
  747. * Move the range to a new center point
  748. * @param {Number} moveTo New center point of the range
  749. */
  750. Range.prototype.moveTo = function(moveTo) {
  751. var center = (this.start + this.end) / 2;
  752. var diff = center - moveTo;
  753. // calculate new start and end
  754. var newStart = this.start - diff;
  755. var newEnd = this.end - diff;
  756. var options = {
  757. animation: false,
  758. byUser: true,
  759. event: null
  760. }
  761. this.setRange(newStart, newEnd, options);
  762. };
  763. module.exports = Range;