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.

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