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.

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