var util = require('../util');
|
|
var hammerUtil = require('../hammerUtil');
|
|
var moment = require('../module/moment');
|
|
var Component = require('./component/Component');
|
|
var DateUtil = require('./DateUtil');
|
|
|
|
/**
|
|
* @constructor Range
|
|
* A Range controls a numeric range with a start and end value.
|
|
* The Range adjusts the range based on mouse events or programmatic changes,
|
|
* and triggers events when the range is changing or has been changed.
|
|
* @param {{dom: Object, domProps: Object, emitter: Emitter}} body
|
|
* @param {Object} [options] See description at Range.setOptions
|
|
*/
|
|
function Range(body, options) {
|
|
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
|
|
var start = now.clone().add(-3, 'days').valueOf();
|
|
var end = now.clone().add(3, 'days').valueOf();
|
|
|
|
if(options === undefined) {
|
|
this.start = start;
|
|
this.end = end;
|
|
} else {
|
|
this.start = options.start || start
|
|
this.end = options.end || end
|
|
}
|
|
|
|
this.rolling = false;
|
|
|
|
this.body = body;
|
|
this.deltaDifference = 0;
|
|
this.scaleOffset = 0;
|
|
this.startToFront = false;
|
|
this.endToFront = true;
|
|
|
|
// default options
|
|
this.defaultOptions = {
|
|
rtl: false,
|
|
start: null,
|
|
end: null,
|
|
moment: moment,
|
|
direction: 'horizontal', // 'horizontal' or 'vertical'
|
|
moveable: true,
|
|
zoomable: true,
|
|
min: null,
|
|
max: null,
|
|
zoomMin: 10, // milliseconds
|
|
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
|
|
rollingMode: {
|
|
follow: false,
|
|
offset: 0.5
|
|
}
|
|
};
|
|
this.options = util.extend({}, this.defaultOptions);
|
|
this.props = {
|
|
touch: {}
|
|
};
|
|
this.animationTimer = null;
|
|
|
|
// drag listeners for dragging
|
|
this.body.emitter.on('panstart', this._onDragStart.bind(this));
|
|
this.body.emitter.on('panmove', this._onDrag.bind(this));
|
|
this.body.emitter.on('panend', this._onDragEnd.bind(this));
|
|
|
|
// mouse wheel for zooming
|
|
this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
|
|
|
|
// pinch to zoom
|
|
this.body.emitter.on('touch', this._onTouch.bind(this));
|
|
this.body.emitter.on('pinch', this._onPinch.bind(this));
|
|
|
|
// on click of rolling mode button
|
|
this.body.dom.rollingModeBtn.addEventListener('click', this.startRolling.bind(this));
|
|
|
|
this.setOptions(options);
|
|
}
|
|
|
|
Range.prototype = new Component();
|
|
|
|
/**
|
|
* Set options for the range controller
|
|
* @param {Object} options Available options:
|
|
* {Number | Date | String} start Start date for the range
|
|
* {Number | Date | String} end End date for the range
|
|
* {Number} min Minimum value for start
|
|
* {Number} max Maximum value for end
|
|
* {Number} zoomMin Set a minimum value for
|
|
* (end - start).
|
|
* {Number} zoomMax Set a maximum value for
|
|
* (end - start).
|
|
* {Boolean} moveable Enable moving of the range
|
|
* by dragging. True by default
|
|
* {Boolean} zoomable Enable zooming of the range
|
|
* by pinching/scrolling. True by default
|
|
*/
|
|
Range.prototype.setOptions = function (options) {
|
|
if (options) {
|
|
// copy the options that we know
|
|
var fields = [
|
|
'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable',
|
|
'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'showCurrentTime', 'rollingMode', 'horizontalScroll'
|
|
];
|
|
util.selectiveExtend(fields, this.options, options);
|
|
|
|
if (options.rollingMode && options.rollingMode.follow) {
|
|
this.startRolling();
|
|
}
|
|
if ('start' in options || 'end' in options) {
|
|
// apply a new range. both start and end are optional
|
|
this.setRange(options.start, options.end);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Test whether direction has a valid value
|
|
* @param {String} direction 'horizontal' or 'vertical'
|
|
*/
|
|
function validateDirection (direction) {
|
|
if (direction != 'horizontal' && direction != 'vertical') {
|
|
throw new TypeError('Unknown direction "' + direction + '". ' +
|
|
'Choose "horizontal" or "vertical".');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start auto refreshing the current time bar
|
|
*/
|
|
Range.prototype.startRolling = function() {
|
|
var me = this;
|
|
|
|
|
|
function update () {
|
|
me.stopRolling();
|
|
me.rolling = true;
|
|
|
|
|
|
var interval = me.end - me.start;
|
|
var t = util.convert(new Date(), 'Date').valueOf();
|
|
|
|
var start = t - interval * (me.options.rollingMode.offset);
|
|
var end = t + interval * (1 - me.options.rollingMode.offset);
|
|
var animation = (me.options && me.options.animation !== undefined) ? me.options.animation : true;
|
|
|
|
var options = {
|
|
animation: false
|
|
};
|
|
me.setRange(start, end, options);
|
|
|
|
// determine interval to refresh
|
|
var scale = me.conversion(me.body.domProps.center.width).scale;
|
|
var interval = 1 / scale / 10;
|
|
if (interval < 30) interval = 30;
|
|
if (interval > 1000) interval = 1000;
|
|
|
|
me.body.dom.rollingModeBtn.style.visibility = "hidden";
|
|
// start a renderTimer to adjust for the new time
|
|
me.currentTimeTimer = setTimeout(update, interval);
|
|
}
|
|
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Stop auto refreshing the current time bar
|
|
*/
|
|
Range.prototype.stopRolling = function() {
|
|
if (this.currentTimeTimer !== undefined) {
|
|
clearTimeout(this.currentTimeTimer);
|
|
this.rolling = false;
|
|
this.body.dom.rollingModeBtn.style.visibility = "visible";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set a new start and end range
|
|
* @param {Date | Number | String} [start]
|
|
* @param {Date | Number | String} [end]
|
|
* @param {Object} options Available options:
|
|
* {Boolean | {duration: number, easingFunction: string}} [animation=false]
|
|
* If true, the range is animated
|
|
* smoothly to the new window. An object can be
|
|
* provided to specify duration and easing function.
|
|
* Default duration is 500 ms, and default easing
|
|
* function is 'easeInOutQuad'.
|
|
* {Boolean} [byUser=false]
|
|
* {Event} event Mouse event
|
|
* {Function} a callback funtion to be executed at the end of this function
|
|
*
|
|
*/
|
|
|
|
Range.prototype.setRange = function(start, end, options, callback) {
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
if (options.byUser !== true) {
|
|
options.byUser = false;
|
|
}
|
|
var me = this;
|
|
var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null;
|
|
var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null;
|
|
this._cancelAnimation();
|
|
|
|
if (options.animation) { // true or an Object
|
|
var initStart = this.start;
|
|
var initEnd = this.end;
|
|
var duration = (typeof options.animation === 'object' && 'duration' in options.animation) ? options.animation.duration : 500;
|
|
var easingName = (typeof options.animation === 'object' && 'easingFunction' in options.animation) ? options.animation.easingFunction : 'easeInOutQuad';
|
|
var easingFunction = util.easingFunctions[easingName];
|
|
if (!easingFunction) {
|
|
throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' +
|
|
'Choose from: ' + Object.keys(util.easingFunctions).join(', '));
|
|
}
|
|
|
|
var initTime = new Date().valueOf();
|
|
var anyChanged = false;
|
|
|
|
var next = function () {
|
|
if (!me.props.touch.dragging) {
|
|
var now = new Date().valueOf();
|
|
var time = now - initTime;
|
|
var ease = easingFunction(time / duration);
|
|
var done = time > duration;
|
|
var s = (done || finalStart === null) ? finalStart : initStart + (finalStart - initStart) * ease;
|
|
var e = (done || finalEnd === null) ? finalEnd : initEnd + (finalEnd - initEnd) * ease;
|
|
|
|
changed = me._applyRange(s, e);
|
|
DateUtil.updateHiddenDates(me.options.moment, me.body, me.options.hiddenDates);
|
|
anyChanged = anyChanged || changed;
|
|
|
|
var params = {
|
|
start: new Date(me.start),
|
|
end: new Date(me.end),
|
|
byUser: options.byUser,
|
|
event: options.event
|
|
}
|
|
|
|
if (changed) {
|
|
me.body.emitter.emit('rangechange', params);
|
|
}
|
|
|
|
if (done) {
|
|
if (anyChanged) {
|
|
me.body.emitter.emit('rangechanged', params);
|
|
if (callback) { return callback() }
|
|
}
|
|
}
|
|
else {
|
|
// animate with as high as possible frame rate, leave 20 ms in between
|
|
// each to prevent the browser from blocking
|
|
me.animationTimer = setTimeout(next, 20);
|
|
}
|
|
}
|
|
};
|
|
|
|
return next();
|
|
}
|
|
else {
|
|
var changed = this._applyRange(finalStart, finalEnd);
|
|
DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);
|
|
if (changed) {
|
|
var params = {
|
|
start: new Date(this.start),
|
|
end: new Date(this.end),
|
|
byUser: options.byUser,
|
|
event: options.event
|
|
};
|
|
|
|
this.body.emitter.emit('rangechange', params);
|
|
clearTimeout( me.timeoutID );
|
|
me.timeoutID = setTimeout( function () {
|
|
me.body.emitter.emit('rangechanged', params);
|
|
}, 200 );
|
|
if (callback) { return callback() }
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the number of milliseconds per pixel.
|
|
*/
|
|
Range.prototype.getMillisecondsPerPixel = function() {
|
|
return (this.end - this.start) / this.body.dom.center.clientWidth;
|
|
}
|
|
|
|
/**
|
|
* Stop an animation
|
|
* @private
|
|
*/
|
|
Range.prototype._cancelAnimation = function () {
|
|
if (this.animationTimer) {
|
|
clearTimeout(this.animationTimer);
|
|
this.animationTimer = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set a new start and end range. This method is the same as setRange, but
|
|
* does not trigger a range change and range changed event, and it returns
|
|
* true when the range is changed
|
|
* @param {Number} [start]
|
|
* @param {Number} [end]
|
|
* @return {Boolean} changed
|
|
* @private
|
|
*/
|
|
Range.prototype._applyRange = function(start, end) {
|
|
var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
|
|
newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
|
|
max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
|
|
min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
|
|
diff;
|
|
|
|
// check for valid number
|
|
if (isNaN(newStart) || newStart === null) {
|
|
throw new Error('Invalid start "' + start + '"');
|
|
}
|
|
if (isNaN(newEnd) || newEnd === null) {
|
|
throw new Error('Invalid end "' + end + '"');
|
|
}
|
|
|
|
// prevent end < start
|
|
if (newEnd < newStart) {
|
|
newEnd = newStart;
|
|
}
|
|
|
|
// prevent start < min
|
|
if (min !== null) {
|
|
if (newStart < min) {
|
|
diff = (min - newStart);
|
|
newStart += diff;
|
|
newEnd += diff;
|
|
|
|
// prevent end > max
|
|
if (max != null) {
|
|
if (newEnd > max) {
|
|
newEnd = max;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// prevent end > max
|
|
if (max !== null) {
|
|
if (newEnd > max) {
|
|
diff = (newEnd - max);
|
|
newStart -= diff;
|
|
newEnd -= diff;
|
|
|
|
// prevent start < min
|
|
if (min != null) {
|
|
if (newStart < min) {
|
|
newStart = min;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// prevent (end-start) < zoomMin
|
|
if (this.options.zoomMin !== null) {
|
|
var zoomMin = parseFloat(this.options.zoomMin);
|
|
if (zoomMin < 0) {
|
|
zoomMin = 0;
|
|
}
|
|
if ((newEnd - newStart) < zoomMin) {
|
|
// compensate for a scale of 0.5 ms
|
|
var compensation = 0.5;
|
|
if ((this.end - this.start) === zoomMin && newStart >= this.start - compensation && newEnd <= this.end) {
|
|
// ignore this action, we are already zoomed to the minimum
|
|
newStart = this.start;
|
|
newEnd = this.end;
|
|
}
|
|
else {
|
|
// zoom to the minimum
|
|
diff = (zoomMin - (newEnd - newStart));
|
|
newStart -= diff / 2;
|
|
newEnd += diff / 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// prevent (end-start) > zoomMax
|
|
if (this.options.zoomMax !== null) {
|
|
var zoomMax = parseFloat(this.options.zoomMax);
|
|
if (zoomMax < 0) {
|
|
zoomMax = 0;
|
|
}
|
|
|
|
if ((newEnd - newStart) > zoomMax) {
|
|
if ((this.end - this.start) === zoomMax && newStart < this.start && newEnd > this.end) {
|
|
// ignore this action, we are already zoomed to the maximum
|
|
newStart = this.start;
|
|
newEnd = this.end;
|
|
}
|
|
else {
|
|
// zoom to the maximum
|
|
diff = ((newEnd - newStart) - zoomMax);
|
|
newStart += diff / 2;
|
|
newEnd -= diff / 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
var changed = (this.start != newStart || this.end != newEnd);
|
|
|
|
// 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)
|
|
if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&
|
|
!((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {
|
|
this.body.emitter.emit('checkRangedItems');
|
|
}
|
|
|
|
this.start = newStart;
|
|
this.end = newEnd;
|
|
return changed;
|
|
};
|
|
|
|
/**
|
|
* Retrieve the current range.
|
|
* @return {Object} An object with start and end properties
|
|
*/
|
|
Range.prototype.getRange = function() {
|
|
return {
|
|
start: this.start,
|
|
end: this.end
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculate the conversion offset and scale for current range, based on
|
|
* the provided width
|
|
* @param {Number} width
|
|
* @returns {{offset: number, scale: number}} conversion
|
|
*/
|
|
Range.prototype.conversion = function (width, totalHidden) {
|
|
return Range.conversion(this.start, this.end, width, totalHidden);
|
|
};
|
|
|
|
/**
|
|
* Static method to calculate the conversion offset and scale for a range,
|
|
* based on the provided start, end, and width
|
|
* @param {Number} start
|
|
* @param {Number} end
|
|
* @param {Number} width
|
|
* @returns {{offset: number, scale: number}} conversion
|
|
*/
|
|
Range.conversion = function (start, end, width, totalHidden) {
|
|
if (totalHidden === undefined) {
|
|
totalHidden = 0;
|
|
}
|
|
if (width != 0 && (end - start != 0)) {
|
|
return {
|
|
offset: start,
|
|
scale: width / (end - start - totalHidden)
|
|
}
|
|
}
|
|
else {
|
|
return {
|
|
offset: 0,
|
|
scale: 1
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start dragging horizontally or vertically
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
Range.prototype._onDragStart = function(event) {
|
|
this.deltaDifference = 0;
|
|
this.previousDelta = 0;
|
|
|
|
// only allow dragging when configured as movable
|
|
if (!this.options.moveable) return;
|
|
|
|
// only start dragging when the mouse is inside the current range
|
|
if (!this._isInsideRange(event)) return;
|
|
|
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
|
// when releasing the fingers in opposite order from the touch screen
|
|
if (!this.props.touch.allowDragging) return;
|
|
|
|
this.stopRolling();
|
|
|
|
this.props.touch.start = this.start;
|
|
this.props.touch.end = this.end;
|
|
this.props.touch.dragging = true;
|
|
|
|
if (this.body.dom.root) {
|
|
this.body.dom.root.style.cursor = 'move';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Perform dragging operation
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
Range.prototype._onDrag = function (event) {
|
|
if (!event) return
|
|
|
|
if (!this.props.touch.dragging) return;
|
|
|
|
// only allow dragging when configured as movable
|
|
if (!this.options.moveable) return;
|
|
|
|
// TODO: this may be redundant in hammerjs2
|
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
|
// when releasing the fingers in opposite order from the touch screen
|
|
if (!this.props.touch.allowDragging) return;
|
|
|
|
var direction = this.options.direction;
|
|
validateDirection(direction);
|
|
var delta = (direction == 'horizontal') ? event.deltaX : event.deltaY;
|
|
delta -= this.deltaDifference;
|
|
var interval = (this.props.touch.end - this.props.touch.start);
|
|
|
|
// normalize dragging speed if cutout is in between.
|
|
var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
|
|
interval -= duration;
|
|
|
|
var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
|
|
|
|
if (this.options.rtl) {
|
|
var diffRange = delta / width * interval;
|
|
} else {
|
|
var diffRange = -delta / width * interval;
|
|
}
|
|
|
|
var newStart = this.props.touch.start + diffRange;
|
|
var newEnd = this.props.touch.end + diffRange;
|
|
|
|
// snapping times away from hidden zones
|
|
var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true);
|
|
var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true);
|
|
if (safeStart != newStart || safeEnd != newEnd) {
|
|
this.deltaDifference += delta;
|
|
this.props.touch.start = safeStart;
|
|
this.props.touch.end = safeEnd;
|
|
this._onDrag(event);
|
|
return;
|
|
}
|
|
|
|
this.previousDelta = delta;
|
|
this._applyRange(newStart, newEnd);
|
|
|
|
|
|
var startDate = new Date(this.start);
|
|
var endDate = new Date(this.end);
|
|
|
|
// fire a rangechange event
|
|
this.body.emitter.emit('rangechange', {
|
|
start: startDate,
|
|
end: endDate,
|
|
byUser: true,
|
|
event: event
|
|
});
|
|
|
|
// fire a panmove event
|
|
this.body.emitter.emit('panmove');
|
|
};
|
|
|
|
/**
|
|
* Stop dragging operation
|
|
* @param {event} event
|
|
* @private
|
|
*/
|
|
Range.prototype._onDragEnd = function (event) {
|
|
if (!this.props.touch.dragging) return;
|
|
|
|
// only allow dragging when configured as movable
|
|
if (!this.options.moveable) return;
|
|
|
|
// TODO: this may be redundant in hammerjs2
|
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
|
// when releasing the fingers in opposite order from the touch screen
|
|
if (!this.props.touch.allowDragging) return;
|
|
|
|
this.props.touch.dragging = false;
|
|
if (this.body.dom.root) {
|
|
this.body.dom.root.style.cursor = 'auto';
|
|
}
|
|
|
|
// fire a rangechanged event
|
|
this.body.emitter.emit('rangechanged', {
|
|
start: new Date(this.start),
|
|
end: new Date(this.end),
|
|
byUser: true,
|
|
event: event
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Event handler for mouse wheel event, used to zoom
|
|
* Code from http://adomas.org/javascript-mouse-wheel/
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
Range.prototype._onMouseWheel = function(event) {
|
|
// retrieve delta
|
|
var delta = 0;
|
|
if (event.wheelDelta) { /* IE/Opera. */
|
|
delta = event.wheelDelta / 120;
|
|
} else if (event.detail) { /* Mozilla case. */
|
|
// In Mozilla, sign of delta is different than in IE.
|
|
// Also, delta is multiple of 3.
|
|
delta = -event.detail / 3;
|
|
}
|
|
|
|
// don't allow zoom when the according key is pressed and the zoomKey option or not zoomable but movable
|
|
if ((this.options.zoomKey && !event[this.options.zoomKey] && this.options.zoomable)
|
|
|| (!this.options.zoomable && this.options.moveable)) {
|
|
if (this.options.horizontalScroll) {
|
|
// Prevent default actions caused by mouse wheel
|
|
// (else the page and timeline both scroll)
|
|
event.preventDefault();
|
|
|
|
// calculate a single scroll jump relative to the range scale
|
|
var diff = delta * (this.end - this.start) / 20;
|
|
// calculate new start and end
|
|
var newStart = this.start - diff;
|
|
var newEnd = this.end - diff;
|
|
|
|
var options = {
|
|
animation: false,
|
|
byUser: true,
|
|
event: event
|
|
}
|
|
this.setRange(newStart, newEnd, options);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// only allow zooming when configured as zoomable and moveable
|
|
if (!(this.options.zoomable && this.options.moveable)) return;
|
|
|
|
// only zoom when the mouse is inside the current range
|
|
if (!this._isInsideRange(event)) return;
|
|
|
|
// If delta is nonzero, handle it.
|
|
// Basically, delta is now positive if wheel was scrolled up,
|
|
// and negative, if wheel was scrolled down.
|
|
if (delta) {
|
|
// perform the zoom action. Delta is normally 1 or -1
|
|
|
|
// adjust a negative delta such that zooming in with delta 0.1
|
|
// equals zooming out with a delta -0.1
|
|
var scale;
|
|
if (delta < 0) {
|
|
scale = 1 - (delta / 5);
|
|
}
|
|
else {
|
|
scale = 1 / (1 + (delta / 5)) ;
|
|
}
|
|
|
|
// calculate center, the date to zoom around
|
|
var pointerDate
|
|
if (this.rolling) {
|
|
pointerDate = this.start + ((this.end - this.start) * this.options.rollingMode.offset);
|
|
} else {
|
|
var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center);
|
|
pointerDate = this._pointerToDate(pointer);
|
|
}
|
|
this.zoom(scale, pointerDate, delta, event);
|
|
|
|
// Prevent default actions caused by mouse wheel
|
|
// (else the page and timeline both scroll)
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start of a touch gesture
|
|
* @private
|
|
*/
|
|
Range.prototype._onTouch = function (event) {
|
|
this.props.touch.start = this.start;
|
|
this.props.touch.end = this.end;
|
|
this.props.touch.allowDragging = true;
|
|
this.props.touch.center = null;
|
|
this.scaleOffset = 0;
|
|
this.deltaDifference = 0;
|
|
};
|
|
|
|
/**
|
|
* Handle pinch event
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
Range.prototype._onPinch = function (event) {
|
|
// only allow zooming when configured as zoomable and moveable
|
|
if (!(this.options.zoomable && this.options.moveable)) return;
|
|
|
|
this.props.touch.allowDragging = false;
|
|
|
|
if (!this.props.touch.center) {
|
|
this.props.touch.center = this.getPointer(event.center, this.body.dom.center);
|
|
}
|
|
|
|
this.stopRolling();
|
|
|
|
var scale = 1 / (event.scale + this.scaleOffset);
|
|
var centerDate = this._pointerToDate(this.props.touch.center);
|
|
|
|
var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
|
|
var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this, centerDate);
|
|
var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
|
|
|
|
// calculate new start and end
|
|
var newStart = (centerDate - hiddenDurationBefore) + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale;
|
|
var newEnd = (centerDate + hiddenDurationAfter) + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale;
|
|
|
|
// snapping times away from hidden zones
|
|
this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times
|
|
this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times
|
|
|
|
var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true);
|
|
var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true);
|
|
if (safeStart != newStart || safeEnd != newEnd) {
|
|
this.props.touch.start = safeStart;
|
|
this.props.touch.end = safeEnd;
|
|
this.scaleOffset = 1 - event.scale;
|
|
newStart = safeStart;
|
|
newEnd = safeEnd;
|
|
}
|
|
|
|
var options = {
|
|
animation: false,
|
|
byUser: true,
|
|
event: event
|
|
}
|
|
this.setRange(newStart, newEnd, options);
|
|
|
|
this.startToFront = false; // revert to default
|
|
this.endToFront = true; // revert to default
|
|
};
|
|
|
|
/**
|
|
* Test whether the mouse from a mouse event is inside the visible window,
|
|
* between the current start and end date
|
|
* @param {Object} event
|
|
* @return {boolean} Returns true when inside the visible window
|
|
* @private
|
|
*/
|
|
Range.prototype._isInsideRange = function(event) {
|
|
// calculate the time where the mouse is, check whether inside
|
|
// and no scroll action should happen.
|
|
var clientX = event.center ? event.center.x : event.clientX;
|
|
if (this.options.rtl) {
|
|
var x = clientX - util.getAbsoluteLeft(this.body.dom.centerContainer);
|
|
} else {
|
|
var x = util.getAbsoluteRight(this.body.dom.centerContainer) - clientX;
|
|
}
|
|
var time = this.body.util.toTime(x);
|
|
|
|
return time >= this.start && time <= this.end;
|
|
};
|
|
|
|
/**
|
|
* Helper function to calculate the center date for zooming
|
|
* @param {{x: Number, y: Number}} pointer
|
|
* @return {number} date
|
|
* @private
|
|
*/
|
|
Range.prototype._pointerToDate = function (pointer) {
|
|
var conversion;
|
|
var direction = this.options.direction;
|
|
|
|
validateDirection(direction);
|
|
|
|
if (direction == 'horizontal') {
|
|
return this.body.util.toTime(pointer.x).valueOf();
|
|
}
|
|
else {
|
|
var height = this.body.domProps.center.height;
|
|
conversion = this.conversion(height);
|
|
return pointer.y / conversion.scale + conversion.offset;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the pointer location relative to the location of the dom element
|
|
* @param {{x: Number, y: Number}} touch
|
|
* @param {Element} element HTML DOM element
|
|
* @return {{x: Number, y: Number}} pointer
|
|
* @private
|
|
*/
|
|
Range.prototype.getPointer = function (touch, element) {
|
|
if (this.options.rtl) {
|
|
return {
|
|
x: util.getAbsoluteRight(element) - touch.x,
|
|
y: touch.y - util.getAbsoluteTop(element)
|
|
};
|
|
} else {
|
|
return {
|
|
x: touch.x - util.getAbsoluteLeft(element),
|
|
y: touch.y - util.getAbsoluteTop(element)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zoom the range the given scale in or out. Start and end date will
|
|
* be adjusted, and the timeline will be redrawn. You can optionally give a
|
|
* date around which to zoom.
|
|
* For example, try scale = 0.9 or 1.1
|
|
* @param {Number} scale Scaling factor. Values above 1 will zoom out,
|
|
* values below 1 will zoom in.
|
|
* @param {Number} [center] Value representing a date around which will
|
|
* be zoomed.
|
|
*/
|
|
Range.prototype.zoom = function(scale, center, delta, event) {
|
|
// if centerDate is not provided, take it half between start Date and end Date
|
|
if (center == null) {
|
|
center = (this.start + this.end) / 2;
|
|
}
|
|
|
|
var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
|
|
var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.options.moment, this.body.hiddenDates, this, center);
|
|
var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
|
|
|
|
// calculate new start and end
|
|
var newStart = (center-hiddenDurationBefore) + (this.start - (center-hiddenDurationBefore)) * scale;
|
|
var newEnd = (center+hiddenDurationAfter) + (this.end - (center+hiddenDurationAfter)) * scale;
|
|
|
|
// snapping times away from hidden zones
|
|
this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
|
|
this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
|
|
var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true);
|
|
var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true);
|
|
if (safeStart != newStart || safeEnd != newEnd) {
|
|
newStart = safeStart;
|
|
newEnd = safeEnd;
|
|
}
|
|
|
|
var options = {
|
|
animation: false,
|
|
byUser: true,
|
|
event: event
|
|
}
|
|
this.setRange(newStart, newEnd, options);
|
|
|
|
this.startToFront = false; // revert to default
|
|
this.endToFront = true; // revert to default
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Move the range with a given delta to the left or right. Start and end
|
|
* value will be adjusted. For example, try delta = 0.1 or -0.1
|
|
* @param {Number} delta Moving amount. Positive value will move right,
|
|
* negative value will move left
|
|
*/
|
|
Range.prototype.move = function(delta) {
|
|
// zoom start Date and end Date relative to the centerDate
|
|
var diff = (this.end - this.start);
|
|
|
|
// apply new values
|
|
var newStart = this.start + diff * delta;
|
|
var newEnd = this.end + diff * delta;
|
|
|
|
// TODO: reckon with min and max range
|
|
|
|
this.start = newStart;
|
|
this.end = newEnd;
|
|
};
|
|
|
|
/**
|
|
* Move the range to a new center point
|
|
* @param {Number} moveTo New center point of the range
|
|
*/
|
|
Range.prototype.moveTo = function(moveTo) {
|
|
var center = (this.start + this.end) / 2;
|
|
|
|
var diff = center - moveTo;
|
|
|
|
// calculate new start and end
|
|
var newStart = this.start - diff;
|
|
var newEnd = this.end - diff;
|
|
|
|
var options = {
|
|
animation: false,
|
|
byUser: true,
|
|
event: null
|
|
}
|
|
this.setRange(newStart, newEnd, options);
|
|
};
|
|
|
|
module.exports = Range;
|