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.

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