UNPKG

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