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.

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