aboutsummaryrefslogtreecommitdiffstats
path: root/library/fullcalendar/fullcalendar.js
diff options
context:
space:
mode:
Diffstat (limited to 'library/fullcalendar/fullcalendar.js')
-rw-r--r--library/fullcalendar/fullcalendar.js1722
1 files changed, 1249 insertions, 473 deletions
diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar/fullcalendar.js
index d6042cc17..cbe67697d 100644
--- a/library/fullcalendar/fullcalendar.js
+++ b/library/fullcalendar/fullcalendar.js
@@ -1,7 +1,7 @@
/*!
- * FullCalendar v2.6.1
+ * FullCalendar v2.7.3
* Docs & License: http://fullcalendar.io/
- * (c) 2015 Adam Shaw
+ * (c) 2016 Adam Shaw
*/
(function(factory) {
@@ -19,8 +19,8 @@
;;
var FC = $.fullCalendar = {
- version: "2.6.1",
- internalApiVersion: 3
+ version: "2.7.3",
+ internalApiVersion: 4
};
var fcViews = FC.views = {};
@@ -262,29 +262,25 @@ function matchCellWidths(els) {
}
-// Turns a container element into a scroller if its contents is taller than the allotted height.
-// Returns true if the element is now a scroller, false otherwise.
-// NOTE: this method is best because it takes weird zooming dimensions into account
-function setPotentialScroller(containerEl, height) {
- containerEl.height(height).addClass('fc-scroller');
-
- // are scrollbars needed?
- if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
- return true;
- }
-
- unsetScroller(containerEl); // undo
- return false;
-}
+// Given one element that resides inside another,
+// Subtracts the height of the inner element from the outer element.
+function subtractInnerElHeight(outerEl, innerEl) {
+ var both = outerEl.add(innerEl);
+ var diff;
+ // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
+ both.css({
+ position: 'relative', // cause a reflow, which will force fresh dimension recalculation
+ left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
+ });
+ diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
+ both.css({ position: '', left: '' }); // undo hack
-// Takes an element that might have been a scroller, and turns it back into a normal element.
-function unsetScroller(containerEl) {
- containerEl.height('').removeClass('fc-scroller');
+ return diff;
}
-/* General DOM Utilities
+/* Element Geom Utilities
----------------------------------------------------------------------------------------------------------------------*/
FC.getOuterRect = getOuterRect;
@@ -309,26 +305,30 @@ function getScrollParent(el) {
// Queries the outer bounding area of a jQuery element.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
-function getOuterRect(el) {
+// Origin is optional.
+function getOuterRect(el, origin) {
var offset = el.offset();
+ var left = offset.left - (origin ? origin.left : 0);
+ var top = offset.top - (origin ? origin.top : 0);
return {
- left: offset.left,
- right: offset.left + el.outerWidth(),
- top: offset.top,
- bottom: offset.top + el.outerHeight()
+ left: left,
+ right: left + el.outerWidth(),
+ top: top,
+ bottom: top + el.outerHeight()
};
}
// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
+// Origin is optional.
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
-function getClientRect(el) {
+function getClientRect(el, origin) {
var offset = el.offset();
var scrollbarWidths = getScrollbarWidths(el);
- var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left;
- var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top;
+ var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
+ var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
return {
left: left,
@@ -341,10 +341,13 @@ function getClientRect(el) {
// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
-function getContentRect(el) {
+// Origin is optional.
+function getContentRect(el, origin) {
var offset = el.offset(); // just outside of border, margin not included
- var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left');
- var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top');
+ var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
+ (origin ? origin.left : 0);
+ var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
+ (origin ? origin.top : 0);
return {
left: left,
@@ -414,13 +417,82 @@ function getCssFloat(el, prop) {
}
+/* Mouse / Touch Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+FC.preventDefault = preventDefault;
+
+
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
function isPrimaryMouseButton(ev) {
return ev.which == 1 && !ev.ctrlKey;
}
-/* Geometry
+function getEvX(ev) {
+ if (ev.pageX !== undefined) {
+ return ev.pageX;
+ }
+ var touches = ev.originalEvent.touches;
+ if (touches) {
+ return touches[0].pageX;
+ }
+}
+
+
+function getEvY(ev) {
+ if (ev.pageY !== undefined) {
+ return ev.pageY;
+ }
+ var touches = ev.originalEvent.touches;
+ if (touches) {
+ return touches[0].pageY;
+ }
+}
+
+
+function getEvIsTouch(ev) {
+ return /^touch/.test(ev.type);
+}
+
+
+function preventSelection(el) {
+ el.addClass('fc-unselectable')
+ .on('selectstart', preventDefault);
+}
+
+
+// Stops a mouse/touch event from doing it's native browser action
+function preventDefault(ev) {
+ ev.preventDefault();
+}
+
+
+// attach a handler to get called when ANY scroll action happens on the page.
+// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
+// http://stackoverflow.com/a/32954565/96342
+// returns `true` on success.
+function bindAnyScroll(handler) {
+ if (window.addEventListener) {
+ window.addEventListener('scroll', handler, true); // useCapture=true
+ return true;
+ }
+ return false;
+}
+
+
+// undoes bindAnyScroll. must pass in the original function.
+// returns `true` on success.
+function unbindAnyScroll(handler) {
+ if (window.removeEventListener) {
+ window.removeEventListener('scroll', handler, true); // useCapture=true
+ return true;
+ }
+ return false;
+}
+
+
+/* General Geometry Utils
----------------------------------------------------------------------------------------------------------------------*/
FC.intersectRects = intersectRects;
@@ -946,22 +1018,21 @@ function proxy(obj, methodName) {
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
-// N milliseconds.
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
-function debounce(func, wait) {
- var timeoutId;
- var args;
- var context;
- var timestamp; // of most recent call
+function debounce(func, wait, immediate) {
+ var timeout, args, context, timestamp, result;
+
var later = function() {
var last = +new Date() - timestamp;
- if (last < wait && last > 0) {
- timeoutId = setTimeout(later, wait - last);
+ if (last < wait) {
+ timeout = setTimeout(later, wait - last);
}
else {
- timeoutId = null;
- func.apply(context, args);
- if (!timeoutId) {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
context = args = null;
}
}
@@ -971,9 +1042,15 @@ function debounce(func, wait) {
context = this;
args = arguments;
timestamp = +new Date();
- if (!timeoutId) {
- timeoutId = setTimeout(later, wait);
+ var callNow = immediate && !timeout;
+ if (!timeout) {
+ timeout = setTimeout(later, wait);
+ }
+ if (callNow) {
+ result = func.apply(context, args);
+ context = args = null;
}
+ return result;
};
}
@@ -1777,61 +1854,162 @@ function extendClass(superClass, members) {
function mixIntoClass(theClass, members) {
- copyOwnProps(members.prototype || members, theClass.prototype); // TODO: copyNativeMethods?
+ copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
}
;;
-var Emitter = FC.Emitter = Class.extend({
+var EmitterMixin = FC.EmitterMixin = {
+
+ // jQuery-ification via $(this) allows a non-DOM object to have
+ // the same event handling capabilities (including namespaces).
+
+
+ on: function(types, handler) {
- callbackHash: null,
+ // handlers are always called with an "event" object as their first param.
+ // sneak the `this` context and arguments into the extra parameter object
+ // and forward them on to the original handler.
+ var intercept = function(ev, extra) {
+ return handler.apply(
+ extra.context || this,
+ extra.args || []
+ );
+ };
+
+ // mimick jQuery's internal "proxy" system (risky, I know)
+ // causing all functions with the same .guid to appear to be the same.
+ // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
+ // this is needed for calling .off with the original non-intercept handler.
+ if (!handler.guid) {
+ handler.guid = $.guid++;
+ }
+ intercept.guid = handler.guid;
+ $(this).on(types, intercept);
- on: function(name, callback) {
- this.getCallbacks(name).add(callback);
return this; // for chaining
},
- off: function(name, callback) {
- this.getCallbacks(name).remove(callback);
+ off: function(types, handler) {
+ $(this).off(types, handler);
+
return this; // for chaining
},
- trigger: function(name) { // args...
- var args = Array.prototype.slice.call(arguments, 1);
+ trigger: function(types) {
+ var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
- this.triggerWith(name, this, args);
+ // pass in "extra" info to the intercept
+ $(this).triggerHandler(types, { args: args });
return this; // for chaining
},
- triggerWith: function(name, context, args) {
- var callbacks = this.getCallbacks(name);
+ triggerWith: function(types, context, args) {
- callbacks.fireWith(context, args);
+ // `triggerHandler` is less reliant on the DOM compared to `trigger`.
+ // pass in "extra" info to the intercept.
+ $(this).triggerHandler(types, { context: context, args: args });
return this; // for chaining
- },
+ }
+};
- getCallbacks: function(name) {
- var callbacks;
+;;
- if (!this.callbackHash) {
- this.callbackHash = {};
- }
+/*
+Utility methods for easily listening to events on another object,
+and more importantly, easily unlistening from them.
+*/
+var ListenerMixin = FC.ListenerMixin = (function() {
+ var guid = 0;
+ var ListenerMixin = {
+
+ listenerId: null,
+
+ /*
+ Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
+ The `callback` will be called with the `this` context of the object that .listenTo is being called on.
+ Can be called:
+ .listenTo(other, eventName, callback)
+ OR
+ .listenTo(other, {
+ eventName1: callback1,
+ eventName2: callback2
+ })
+ */
+ listenTo: function(other, arg, callback) {
+ if (typeof arg === 'object') { // given dictionary of callbacks
+ for (var eventName in arg) {
+ if (arg.hasOwnProperty(eventName)) {
+ this.listenTo(other, eventName, arg[eventName]);
+ }
+ }
+ }
+ else if (typeof arg === 'string') {
+ other.on(
+ arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
+ $.proxy(callback, this) // always use `this` context
+ // the usually-undesired jQuery guid behavior doesn't matter,
+ // because we always unbind via namespace
+ );
+ }
+ },
+
+ /*
+ Causes the current object to stop listening to events on the `other` object.
+ `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
+ */
+ stopListeningTo: function(other, eventName) {
+ other.off((eventName || '') + '.' + this.getListenerNamespace());
+ },
- callbacks = this.callbackHash[name];
- if (!callbacks) {
- callbacks = this.callbackHash[name] = $.Callbacks();
+ /*
+ Returns a string, unique to this object, to be used for event namespacing
+ */
+ getListenerNamespace: function() {
+ if (this.listenerId == null) {
+ this.listenerId = guid++;
+ }
+ return '_listener' + this.listenerId;
}
- return callbacks;
+ };
+ return ListenerMixin;
+})();
+;;
+
+// simple class for toggle a `isIgnoringMouse` flag on delay
+// initMouseIgnoring must first be called, with a millisecond delay setting.
+var MouseIgnorerMixin = {
+
+ isIgnoringMouse: false, // bool
+ delayUnignoreMouse: null, // method
+
+
+ initMouseIgnoring: function(delay) {
+ this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
+ },
+
+
+ // temporarily ignore mouse actions on segments
+ tempIgnoreMouse: function() {
+ this.isIgnoringMouse = true;
+ this.delayUnignoreMouse();
+ },
+
+
+ // delayUnignoreMouse eventually calls this
+ unignoreMouse: function() {
+ this.isIgnoringMouse = false;
}
-});
+};
+
;;
/* A rectangular panel that is absolutely positioned over other content
@@ -1848,12 +2026,11 @@ Options:
- hide (callback)
*/
-var Popover = Class.extend({
+var Popover = Class.extend(ListenerMixin, {
isHidden: true,
options: null,
el: null, // the container element for the popover. generated by this object
- documentMousedownProxy: null, // document mousedown handler bound to `this`
margin: 10, // the space required between the popover and the edges of the scroll container
@@ -1907,7 +2084,7 @@ var Popover = Class.extend({
});
if (options.autoHide) {
- $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
+ this.listenTo($(document), 'mousedown', this.documentMousedown);
}
},
@@ -1930,7 +2107,7 @@ var Popover = Class.extend({
this.el = null;
}
- $(document).off('mousedown', this.documentMousedownProxy);
+ this.stopListeningTo($(document), 'mousedown');
},
@@ -2243,257 +2420,421 @@ var CoordCache = FC.CoordCache = Class.extend({
----------------------------------------------------------------------------------------------------------------------*/
// TODO: use Emitter
-var DragListener = FC.DragListener = Class.extend({
+var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
options: null,
- isListening: false,
- isDragging: false,
+ // for IE8 bug-fighting behavior
+ subjectEl: null,
+ subjectHref: null,
// coordinates of the initial mousedown
originX: null,
originY: null,
- // handler attached to the document, bound to the DragListener's `this`
- mousemoveProxy: null,
- mouseupProxy: null,
+ // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
+ // TODO: do this for wrappers that have overflow:hidden as well.
+ scrollEl: null,
- // for IE8 bug-fighting behavior, for now
- subjectEl: null, // the element being draged. optional
- subjectHref: null,
+ isInteracting: false,
+ isDistanceSurpassed: false,
+ isDelayEnded: false,
+ isDragging: false,
+ isTouch: false,
- scrollEl: null,
- scrollBounds: null, // { top, bottom, left, right }
- scrollTopVel: null, // pixels per second
- scrollLeftVel: null, // pixels per second
- scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
- scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
+ delay: null,
+ delayTimeoutId: null,
+ minDistance: null,
- scrollSensitivity: 30, // pixels from edge for scrolling to start
- scrollSpeed: 200, // pixels per second, at maximum speed
- scrollIntervalMs: 50, // millisecond wait between scroll increment
+ handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
constructor: function(options) {
- options = options || {};
- this.options = options;
- this.subjectEl = options.subjectEl;
+ this.options = options || {};
+ this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
+ this.initMouseIgnoring(500);
},
- // Call this when the user does a mousedown. Will probably lead to startListening
- mousedown: function(ev) {
- if (isPrimaryMouseButton(ev)) {
+ // Interaction (high-level)
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ startInteraction: function(ev, extraOptions) {
+ var isTouch = getEvIsTouch(ev);
+
+ if (ev.type === 'mousedown') {
+ if (this.isIgnoringMouse) {
+ return;
+ }
+ else if (!isPrimaryMouseButton(ev)) {
+ return;
+ }
+ else {
+ ev.preventDefault(); // prevents native selection in most browsers
+ }
+ }
+
+ if (!this.isInteracting) {
+
+ // process options
+ extraOptions = extraOptions || {};
+ this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
+ this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
+ this.subjectEl = this.options.subjectEl;
+
+ this.isInteracting = true;
+ this.isTouch = isTouch;
+ this.isDelayEnded = false;
+ this.isDistanceSurpassed = false;
- ev.preventDefault(); // prevents native selection in most browsers
+ this.originX = getEvX(ev);
+ this.originY = getEvY(ev);
+ this.scrollEl = getScrollParent($(ev.target));
- this.startListening(ev);
+ this.bindHandlers();
+ this.initAutoScroll();
+ this.handleInteractionStart(ev);
+ this.startDelay(ev);
- // start the drag immediately if there is no minimum distance for a drag start
- if (!this.options.distance) {
- this.startDrag(ev);
+ if (!this.minDistance) {
+ this.handleDistanceSurpassed(ev);
}
}
},
- // Call this to start tracking mouse movements
- startListening: function(ev) {
- var scrollParent;
+ handleInteractionStart: function(ev) {
+ this.trigger('interactionStart', ev);
+ },
- if (!this.isListening) {
- // grab scroll container and attach handler
- if (ev && this.options.scroll) {
- scrollParent = getScrollParent($(ev.target));
- if (!scrollParent.is(window) && !scrollParent.is(document)) {
- this.scrollEl = scrollParent;
+ endInteraction: function(ev, isCancelled) {
+ if (this.isInteracting) {
+ this.endDrag(ev);
- // scope to `this`, and use `debounce` to make sure rapid calls don't happen
- this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
- this.scrollEl.on('scroll', this.scrollHandlerProxy);
- }
+ if (this.delayTimeoutId) {
+ clearTimeout(this.delayTimeoutId);
+ this.delayTimeoutId = null;
}
- $(document)
- .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
- .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
- .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
+ this.destroyAutoScroll();
+ this.unbindHandlers();
+
+ this.isInteracting = false;
+ this.handleInteractionEnd(ev, isCancelled);
- if (ev) {
- this.originX = ev.pageX;
- this.originY = ev.pageY;
+ // a touchstart+touchend on the same element will result in the following addition simulated events:
+ // mouseover + mouseout + click
+ // let's ignore these bogus events
+ if (this.isTouch) {
+ this.tempIgnoreMouse();
}
- else {
- // if no starting information was given, origin will be the topleft corner of the screen.
- // if so, dx/dy in the future will be the absolute coordinates.
- this.originX = 0;
- this.originY = 0;
+ }
+ },
+
+
+ handleInteractionEnd: function(ev, isCancelled) {
+ this.trigger('interactionEnd', ev, isCancelled || false);
+ },
+
+
+ // Binding To DOM
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ bindHandlers: function() {
+ var _this = this;
+ var touchStartIgnores = 1;
+
+ if (this.isTouch) {
+ this.listenTo($(document), {
+ touchmove: this.handleTouchMove,
+ touchend: this.endInteraction,
+ touchcancel: this.endInteraction,
+
+ // Sometimes touchend doesn't fire
+ // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
+ // If another touchstart happens, we know it's bogus, so cancel the drag.
+ // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
+ touchstart: function(ev) {
+ if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
+ touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
+ }
+ else {
+ _this.endInteraction(ev, true); // isCancelled=true
+ }
+ }
+ });
+
+ // listen to ALL scroll actions on the page
+ if (
+ !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
+ this.scrollEl // otherwise, attach a single handler to this
+ ) {
+ this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
}
+ }
+ else {
+ this.listenTo($(document), {
+ mousemove: this.handleMouseMove,
+ mouseup: this.endInteraction
+ });
+ }
- this.isListening = true;
- this.listenStart(ev);
+ this.listenTo($(document), {
+ selectstart: preventDefault, // don't allow selection while dragging
+ contextmenu: preventDefault // long taps would open menu on Chrome dev tools
+ });
+ },
+
+
+ unbindHandlers: function() {
+ this.stopListeningTo($(document));
+
+ // unbind scroll listening
+ unbindAnyScroll(this.handleTouchScrollProxy);
+ if (this.scrollEl) {
+ this.stopListeningTo(this.scrollEl, 'scroll');
+ }
+ },
+
+
+ // Drag (high-level)
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // extraOptions ignored if drag already started
+ startDrag: function(ev, extraOptions) {
+ this.startInteraction(ev, extraOptions); // ensure interaction began
+
+ if (!this.isDragging) {
+ this.isDragging = true;
+ this.handleDragStart(ev);
}
},
- // Called when drag listening has started (but a real drag has not necessarily began)
- listenStart: function(ev) {
- this.trigger('listenStart', ev);
+ handleDragStart: function(ev) {
+ this.trigger('dragStart', ev);
+ this.initHrefHack();
},
- // Called when the user moves the mouse
- mousemove: function(ev) {
- var dx = ev.pageX - this.originX;
- var dy = ev.pageY - this.originY;
- var minDistance;
+ handleMove: function(ev) {
+ var dx = getEvX(ev) - this.originX;
+ var dy = getEvY(ev) - this.originY;
+ var minDistance = this.minDistance;
var distanceSq; // current distance from the origin, squared
- if (!this.isDragging) { // if not already dragging...
- // then start the drag if the minimum distance criteria is met
- minDistance = this.options.distance || 1;
+ if (!this.isDistanceSurpassed) {
distanceSq = dx * dx + dy * dy;
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
- this.startDrag(ev);
+ this.handleDistanceSurpassed(ev);
}
}
if (this.isDragging) {
- this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
+ this.handleDrag(dx, dy, ev);
}
},
- // Call this to initiate a legitimate drag.
- // This function is called internally from this class, but can also be called explicitly from outside
- startDrag: function(ev) {
+ // Called while the mouse is being moved and when we know a legitimate drag is taking place
+ handleDrag: function(dx, dy, ev) {
+ this.trigger('drag', dx, dy, ev);
+ this.updateAutoScroll(ev); // will possibly cause scrolling
+ },
- if (!this.isListening) { // startDrag must have manually initiated
- this.startListening();
- }
- if (!this.isDragging) {
- this.isDragging = true;
- this.dragStart(ev);
+ endDrag: function(ev) {
+ if (this.isDragging) {
+ this.isDragging = false;
+ this.handleDragEnd(ev);
}
},
- // Called when the actual drag has started (went beyond minDistance)
- dragStart: function(ev) {
- var subjectEl = this.subjectEl;
+ handleDragEnd: function(ev) {
+ this.trigger('dragEnd', ev);
+ this.destroyHrefHack();
+ },
- this.trigger('dragStart', ev);
- // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
- if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
- subjectEl.removeAttr('href');
+ // Delay
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ startDelay: function(initialEv) {
+ var _this = this;
+
+ if (this.delay) {
+ this.delayTimeoutId = setTimeout(function() {
+ _this.handleDelayEnd(initialEv);
+ }, this.delay);
+ }
+ else {
+ this.handleDelayEnd(initialEv);
}
},
- // Called while the mouse is being moved and when we know a legitimate drag is taking place
- drag: function(dx, dy, ev) {
- this.trigger('drag', dx, dy, ev);
- this.updateScroll(ev); // will possibly cause scrolling
+ handleDelayEnd: function(initialEv) {
+ this.isDelayEnded = true;
+
+ if (this.isDistanceSurpassed) {
+ this.startDrag(initialEv);
+ }
},
- // Called when the user does a mouseup
- mouseup: function(ev) {
- this.stopListening(ev);
+ // Distance
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ handleDistanceSurpassed: function(ev) {
+ this.isDistanceSurpassed = true;
+
+ if (this.isDelayEnded) {
+ this.startDrag(ev);
+ }
},
- // Called when the drag is over. Will not cause listening to stop however.
- // A concluding 'cellOut' event will NOT be triggered.
- stopDrag: function(ev) {
+ // Mouse / Touch
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ handleTouchMove: function(ev) {
+ // prevent inertia and touchmove-scrolling while dragging
if (this.isDragging) {
- this.stopScrolling();
- this.dragStop(ev);
- this.isDragging = false;
+ ev.preventDefault();
}
+
+ this.handleMove(ev);
},
- // Called when dragging has been stopped
- dragStop: function(ev) {
- var _this = this;
+ handleMouseMove: function(ev) {
+ this.handleMove(ev);
+ },
- this.trigger('dragStop', ev);
- // restore a mousedown'd <a>'s href (for IE8 bug)
- setTimeout(function() { // must be outside of the click's execution
- if (_this.subjectHref) {
- _this.subjectEl.attr('href', _this.subjectHref);
- }
- }, 0);
- },
+ // Scrolling (unrelated to auto-scroll)
+ // -----------------------------------------------------------------------------------------------------------------
- // Call this to stop listening to the user's mouse events
- stopListening: function(ev) {
- this.stopDrag(ev); // if there's a current drag, kill it
+ handleTouchScroll: function(ev) {
+ // if the drag is being initiated by touch, but a scroll happens before
+ // the drag-initiating delay is over, cancel the drag
+ if (!this.isDragging) {
+ this.endInteraction(ev, true); // isCancelled=true
+ }
+ },
- if (this.isListening) {
- // remove the scroll handler if there is a scrollEl
- if (this.scrollEl) {
- this.scrollEl.off('scroll', this.scrollHandlerProxy);
- this.scrollHandlerProxy = null;
- }
+ // <A> HREF Hack
+ // -----------------------------------------------------------------------------------------------------------------
- $(document)
- .off('mousemove', this.mousemoveProxy)
- .off('mouseup', this.mouseupProxy)
- .off('selectstart', this.preventDefault);
- this.mousemoveProxy = null;
- this.mouseupProxy = null;
+ initHrefHack: function() {
+ var subjectEl = this.subjectEl;
- this.isListening = false;
- this.listenStop(ev);
+ // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
+ if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
+ subjectEl.removeAttr('href');
}
},
- // Called when drag listening has stopped
- listenStop: function(ev) {
- this.trigger('listenStop', ev);
+ destroyHrefHack: function() {
+ var subjectEl = this.subjectEl;
+ var subjectHref = this.subjectHref;
+
+ // restore a mousedown'd <a>'s href (for IE8 bug)
+ setTimeout(function() { // must be outside of the click's execution
+ if (subjectHref) {
+ subjectEl.attr('href', subjectHref);
+ }
+ }, 0);
},
+ // Utils
+ // -----------------------------------------------------------------------------------------------------------------
+
+
// Triggers a callback. Calls a function in the option hash of the same name.
// Arguments beyond the first `name` are forwarded on.
trigger: function(name) {
if (this.options[name]) {
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
}
- },
+ // makes _methods callable by event name. TODO: kill this
+ if (this['_' + name]) {
+ this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ }
+
+
+});
+
+;;
+/*
+this.scrollEl is set in DragListener
+*/
+DragListener.mixin({
+ isAutoScroll: false,
- // Stops a given mouse event from doing it's native browser action. In our case, text selection.
- preventDefault: function(ev) {
- ev.preventDefault();
+ scrollBounds: null, // { top, bottom, left, right }
+ scrollTopVel: null, // pixels per second
+ scrollLeftVel: null, // pixels per second
+ scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
+
+ // defaults
+ scrollSensitivity: 30, // pixels from edge for scrolling to start
+ scrollSpeed: 200, // pixels per second, at maximum speed
+ scrollIntervalMs: 50, // millisecond wait between scroll increment
+
+
+ initAutoScroll: function() {
+ var scrollEl = this.scrollEl;
+
+ this.isAutoScroll =
+ this.options.scroll &&
+ scrollEl &&
+ !scrollEl.is(window) &&
+ !scrollEl.is(document);
+
+ if (this.isAutoScroll) {
+ // debounce makes sure rapid calls don't happen
+ this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
+ }
},
- /* Scrolling
- ------------------------------------------------------------------------------------------------------------------*/
+ destroyAutoScroll: function() {
+ this.endAutoScroll(); // kill any animation loop
+
+ // remove the scroll handler if there is a scrollEl
+ if (this.isAutoScroll) {
+ this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
+ }
+ },
// Computes and stores the bounding rectangle of scrollEl
computeScrollBounds: function() {
- var el = this.scrollEl;
-
- this.scrollBounds = el ? getOuterRect(el) : null;
+ if (this.isAutoScroll) {
+ this.scrollBounds = getOuterRect(this.scrollEl);
// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
+ }
},
// Called when the dragging is in progress and scrolling should be updated
- updateScroll: function(ev) {
+ updateAutoScroll: function(ev) {
var sensitivity = this.scrollSensitivity;
var bounds = this.scrollBounds;
var topCloseness, bottomCloseness;
@@ -2504,10 +2845,10 @@ var DragListener = FC.DragListener = Class.extend({
if (bounds) { // only scroll if scrollEl exists
// compute closeness to edges. valid range is from 0.0 - 1.0
- topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
- bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
- leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
- rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
+ topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
+ bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
+ leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
+ rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
// translate vertical closeness into velocity.
// mouse must be completely in bounds for velocity to happen.
@@ -2594,38 +2935,36 @@ var DragListener = FC.DragListener = Class.extend({
// if scrolled all the way, which causes the vels to be zero, stop the animation loop
if (!this.scrollTopVel && !this.scrollLeftVel) {
- this.stopScrolling();
+ this.endAutoScroll();
}
},
// Kills any existing scrolling animation loop
- stopScrolling: function() {
+ endAutoScroll: function() {
if (this.scrollIntervalId) {
clearInterval(this.scrollIntervalId);
this.scrollIntervalId = null;
- // when all done with scrolling, recompute positions since they probably changed
- this.scrollStop();
+ this.handleScrollEnd();
}
},
// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
- scrollHandler: function() {
+ handleDebouncedScroll: function() {
// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
if (!this.scrollIntervalId) {
- this.scrollStop();
+ this.handleScrollEnd();
}
},
// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
- scrollStop: function() {
+ handleScrollEnd: function() {
}
});
-
;;
/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
@@ -2654,18 +2993,16 @@ var HitDragListener = DragListener.extend({
// Called when drag listening starts (but a real drag has not necessarily began).
// ev might be undefined if dragging was started manually.
- listenStart: function(ev) {
+ handleInteractionStart: function(ev) {
var subjectEl = this.subjectEl;
var subjectRect;
var origPoint;
var point;
- DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
-
this.computeCoords();
if (ev) {
- origPoint = { left: ev.pageX, top: ev.pageY };
+ origPoint = { left: getEvX(ev), top: getEvY(ev) };
point = origPoint;
// constrain the point to bounds of the element being dragged
@@ -2695,61 +3032,64 @@ var HitDragListener = DragListener.extend({
this.origHit = null;
this.coordAdjust = null;
}
+
+ // call the super-method. do it after origHit has been computed
+ DragListener.prototype.handleInteractionStart.apply(this, arguments);
},
// Recomputes the drag-critical positions of elements
computeCoords: function() {
this.component.prepareHits();
- this.computeScrollBounds(); // why is this here???
+ this.computeScrollBounds(); // why is this here??????
},
// Called when the actual drag has started
- dragStart: function(ev) {
+ handleDragStart: function(ev) {
var hit;
- DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
+ DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
// might be different from this.origHit if the min-distance is large
- hit = this.queryHit(ev.pageX, ev.pageY);
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
// report the initial hit the mouse is over
// especially important if no min-distance and drag starts immediately
if (hit) {
- this.hitOver(hit);
+ this.handleHitOver(hit);
}
},
// Called when the drag moves
- drag: function(dx, dy, ev) {
+ handleDrag: function(dx, dy, ev) {
var hit;
- DragListener.prototype.drag.apply(this, arguments); // call the super-method
+ DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
- hit = this.queryHit(ev.pageX, ev.pageY);
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
if (this.hit) {
- this.hitOut();
+ this.handleHitOut();
}
if (hit) {
- this.hitOver(hit);
+ this.handleHitOver(hit);
}
}
},
// Called when dragging has been stopped
- dragStop: function() {
- this.hitDone();
- DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
+ handleDragEnd: function() {
+ this.handleHitDone();
+ DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
},
// Called when a the mouse has just moved over a new hit
- hitOver: function(hit) {
+ handleHitOver: function(hit) {
var isOrig = isHitsEqual(hit, this.origHit);
this.hit = hit;
@@ -2759,26 +3099,26 @@ var HitDragListener = DragListener.extend({
// Called when the mouse has just moved out of a hit
- hitOut: function() {
+ handleHitOut: function() {
if (this.hit) {
this.trigger('hitOut', this.hit);
- this.hitDone();
+ this.handleHitDone();
this.hit = null;
}
},
// Called after a hitOut. Also called before a dragStop
- hitDone: function() {
+ handleHitDone: function() {
if (this.hit) {
this.trigger('hitDone', this.hit);
}
},
- // Called when drag listening has stopped
- listenStop: function() {
- DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
+ // Called when the interaction ends, whether there was a real drag or not
+ handleInteractionEnd: function() {
+ DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
this.origHit = null;
this.hit = null;
@@ -2788,8 +3128,8 @@ var HitDragListener = DragListener.extend({
// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
- scrollStop: function() {
- DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method
+ handleScrollEnd: function() {
+ DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
this.computeCoords(); // hits' absolute positions will be in new places. recompute
},
@@ -2844,7 +3184,7 @@ function isHitPropsWithin(subHit, superHit) {
/* Creates a clone of an element and lets it track the mouse as it moves
----------------------------------------------------------------------------------------------------------------------*/
-var MouseFollower = Class.extend({
+var MouseFollower = Class.extend(ListenerMixin, {
options: null,
@@ -2856,16 +3196,14 @@ var MouseFollower = Class.extend({
top0: null,
left0: null,
- // the initial position of the mouse
- mouseY0: null,
- mouseX0: null,
+ // the absolute coordinates of the initiating touch/mouse action
+ y0: null,
+ x0: null,
// the number of pixels the mouse has moved from its initial position
topDelta: null,
leftDelta: null,
- mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
-
isFollowing: false,
isHidden: false,
isAnimating: false, // doing the revert animation?
@@ -2882,8 +3220,8 @@ var MouseFollower = Class.extend({
if (!this.isFollowing) {
this.isFollowing = true;
- this.mouseY0 = ev.pageY;
- this.mouseX0 = ev.pageX;
+ this.y0 = getEvY(ev);
+ this.x0 = getEvX(ev);
this.topDelta = 0;
this.leftDelta = 0;
@@ -2891,7 +3229,12 @@ var MouseFollower = Class.extend({
this.updatePosition();
}
- $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
+ if (getEvIsTouch(ev)) {
+ this.listenTo($(document), 'touchmove', this.handleMove);
+ }
+ else {
+ this.listenTo($(document), 'mousemove', this.handleMove);
+ }
}
},
@@ -2916,7 +3259,7 @@ var MouseFollower = Class.extend({
if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
this.isFollowing = false;
- $(document).off('mousemove', this.mousemoveProxy);
+ this.stopListeningTo($(document));
if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
this.isAnimating = true;
@@ -2942,6 +3285,7 @@ var MouseFollower = Class.extend({
if (!el) {
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
el = this.el = this.sourceEl.clone()
+ .addClass(this.options.additionalClass || '')
.css({
position: 'absolute',
visibility: '', // in case original element was hidden (commonly through hideEvents())
@@ -2953,8 +3297,13 @@ var MouseFollower = Class.extend({
height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
opacity: this.options.opacity || '',
zIndex: this.options.zIndex
- })
- .appendTo(this.parentEl);
+ });
+
+ // we don't want long taps or any mouse interaction causing selection/menus.
+ // would use preventSelection(), but that prevents selectstart, causing problems.
+ el.addClass('fc-unselectable');
+
+ el.appendTo(this.parentEl);
}
return el;
@@ -2994,9 +3343,9 @@ var MouseFollower = Class.extend({
// Gets called when the user moves the mouse
- mousemove: function(ev) {
- this.topDelta = ev.pageY - this.mouseY0;
- this.leftDelta = ev.pageX - this.mouseX0;
+ handleMove: function(ev) {
+ this.topDelta = getEvY(ev) - this.y0;
+ this.leftDelta = getEvX(ev) - this.x0;
if (!this.isHidden) {
this.updatePosition();
@@ -3031,7 +3380,7 @@ var MouseFollower = Class.extend({
/* An abstract class comprised of a "grid" of areas that each represent a specific datetime
----------------------------------------------------------------------------------------------------------------------*/
-var Grid = FC.Grid = Class.extend({
+var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
view: null, // a View object
isRTL: null, // shortcut to the view's isRTL option
@@ -3042,8 +3391,6 @@ var Grid = FC.Grid = Class.extend({
el: null, // the containing element
elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
- externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
-
// derived from options
eventTimeFormat: null,
displayEventTime: null,
@@ -3056,13 +3403,19 @@ var Grid = FC.Grid = Class.extend({
// TODO: port isTimeScale into same system?
largeUnit: null,
+ dayDragListener: null,
+ segDragListener: null,
+ segResizeListener: null,
+ externalDragListener: null,
+
constructor: function(view) {
this.view = view;
this.isRTL = view.opt('isRTL');
-
this.elsByFill = {};
- this.externalDragStartProxy = proxy(this, 'externalDragStart');
+
+ this.dayDragListener = this.buildDayDragListener();
+ this.initMouseIgnoring();
},
@@ -3195,26 +3548,33 @@ var Grid = FC.Grid = Class.extend({
// Sets the container element that the grid should render inside of.
// Does other DOM-related initializations.
setElement: function(el) {
- var _this = this;
-
this.el = el;
+ preventSelection(el);
+
+ this.bindDayHandler('touchstart', this.dayTouchStart);
+ this.bindDayHandler('mousedown', this.dayMousedown);
+
+ // attach event-element-related handlers. in Grid.events
+ // same garbage collection note as above.
+ this.bindSegHandlers();
+
+ this.bindGlobalHandlers();
+ },
+
+
+ bindDayHandler: function(name, handler) {
+ var _this = this;
// attach a handler to the grid's root element.
// jQuery will take care of unregistering them when removeElement gets called.
- el.on('mousedown', function(ev) {
+ this.el.on(name, function(ev) {
if (
!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
) {
- _this.dayMousedown(ev);
+ return handler.call(_this, ev);
}
});
-
- // attach event-element-related handlers. in Grid.events
- // same garbage collection note as above.
- this.bindSegHandlers();
-
- this.bindGlobalHandlers();
},
@@ -3222,6 +3582,7 @@ var Grid = FC.Grid = Class.extend({
// DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
removeElement: function() {
this.unbindGlobalHandlers();
+ this.clearDragListeners();
this.el.remove();
@@ -3254,18 +3615,47 @@ var Grid = FC.Grid = Class.extend({
// Binds DOM handlers to elements that reside outside the grid, such as the document
bindGlobalHandlers: function() {
- $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
+ this.listenTo($(document), {
+ dragstart: this.externalDragStart, // jqui
+ sortstart: this.externalDragStart // jqui
+ });
},
// Unbinds DOM handlers from elements that reside outside the grid
unbindGlobalHandlers: function() {
- $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
+ this.stopListeningTo($(document));
},
// Process a mousedown on an element that represents a day. For day clicking and selecting.
dayMousedown: function(ev) {
+ if (!this.isIgnoringMouse) {
+ this.dayDragListener.startInteraction(ev, {
+ //distance: 5, // needs more work if we want dayClick to fire correctly
+ });
+ }
+ },
+
+
+ dayTouchStart: function(ev) {
+ var view = this.view;
+
+ // HACK to prevent a user's clickaway for unselecting a range or an event
+ // from causing a dayClick.
+ if (view.isSelected || view.selectedEvent) {
+ this.tempIgnoreMouse();
+ }
+
+ this.dayDragListener.startInteraction(ev, {
+ delay: this.view.opt('longPressDelay')
+ });
+ },
+
+
+ // Creates a listener that tracks the user's drag across day elements.
+ // For day clicking and selecting.
+ buildDayDragListener: function() {
var _this = this;
var view = this.view;
var isSelectable = view.opt('selectable');
@@ -3276,14 +3666,21 @@ var Grid = FC.Grid = Class.extend({
// if the drag ends on the same day, it is a 'dayClick'.
// if 'selectable' is enabled, this listener also detects selections.
var dragListener = new HitDragListener(this, {
- //distance: 5, // needs more work if we want dayClick to fire correctly
scroll: view.opt('dragScroll'),
+ interactionStart: function() {
+ dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
+ },
dragStart: function() {
view.unselect(); // since we could be rendering a new selection, we want to clear any old one
},
hitOver: function(hit, isOrig, origHit) {
if (origHit) { // click needs to have started on a hit
- dayClickHit = isOrig ? hit : null; // single-hit selection is a day click
+
+ // if user dragged to another cell at any point, it can no longer be a dayClick
+ if (!isOrig) {
+ dayClickHit = null;
+ }
+
if (isSelectable) {
selectionSpan = _this.computeSelection(
_this.getHitSpan(origHit),
@@ -3304,23 +3701,46 @@ var Grid = FC.Grid = Class.extend({
_this.unrenderSelection();
enableCursor();
},
- listenStop: function(ev) {
- if (dayClickHit) {
- view.triggerDayClick(
- _this.getHitSpan(dayClickHit),
- _this.getHitEl(dayClickHit),
- ev
- );
- }
- if (selectionSpan) {
- // the selection will already have been rendered. just report it
- view.reportSelection(selectionSpan, ev);
+ interactionEnd: function(ev, isCancelled) {
+ if (!isCancelled) {
+ if (
+ dayClickHit &&
+ !_this.isIgnoringMouse // see hack in dayTouchStart
+ ) {
+ view.triggerDayClick(
+ _this.getHitSpan(dayClickHit),
+ _this.getHitEl(dayClickHit),
+ ev
+ );
+ }
+ if (selectionSpan) {
+ // the selection will already have been rendered. just report it
+ view.reportSelection(selectionSpan, ev);
+ }
+ enableCursor();
}
- enableCursor();
}
});
- dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
+ return dragListener;
+ },
+
+
+ // Kills all in-progress dragging.
+ // Useful for when public API methods that result in re-rendering are invoked during a drag.
+ // Also useful for when touch devices misbehave and don't fire their touchend.
+ clearDragListeners: function() {
+ this.dayDragListener.endInteraction();
+
+ if (this.segDragListener) {
+ this.segDragListener.endInteraction(); // will clear this.segDragListener
+ }
+ if (this.segResizeListener) {
+ this.segResizeListener.endInteraction(); // will clear this.segResizeListener
+ }
+ if (this.externalDragListener) {
+ this.externalDragListener.endInteraction(); // will clear this.externalDragListener
+ }
},
@@ -3330,10 +3750,11 @@ var Grid = FC.Grid = Class.extend({
// Renders a mock event at the given event location, which contains zoned start/end properties.
+ // Returns all mock event elements.
renderEventLocationHelper: function(eventLocation, sourceSeg) {
var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
- this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
+ return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
},
@@ -3361,6 +3782,7 @@ var Grid = FC.Grid = Class.extend({
// Renders a mock event. Given zoned event date properties.
+ // Must return all mock event elements.
renderHelper: function(eventLocation, sourceSeg) {
// subclasses must implement
},
@@ -3640,7 +4062,8 @@ Grid.mixin({
// Unrenders all events currently rendered on the grid
unrenderEvents: function() {
- this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
+ this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
+ this.clearDragListeners();
this.unrenderFgSegs();
this.unrenderBgSegs();
@@ -3768,48 +4191,44 @@ Grid.mixin({
// Attaches event-element-related handlers to the container element and leverage bubbling
bindSegHandlers: function() {
+ this.bindSegHandler('touchstart', this.handleSegTouchStart);
+ this.bindSegHandler('touchend', this.handleSegTouchEnd);
+ this.bindSegHandler('mouseenter', this.handleSegMouseover);
+ this.bindSegHandler('mouseleave', this.handleSegMouseout);
+ this.bindSegHandler('mousedown', this.handleSegMousedown);
+ this.bindSegHandler('click', this.handleSegClick);
+ },
+
+
+ // Executes a handler for any a user-interaction on a segment.
+ // Handler gets called with (seg, ev), and with the `this` context of the Grid
+ bindSegHandler: function(name, handler) {
var _this = this;
- var view = this.view;
- $.each(
- {
- mouseenter: function(seg, ev) {
- _this.triggerSegMouseover(seg, ev);
- },
- mouseleave: function(seg, ev) {
- _this.triggerSegMouseout(seg, ev);
- },
- click: function(seg, ev) {
- return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
- },
- mousedown: function(seg, ev) {
- if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
- _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer'));
- }
- else if (view.isEventDraggable(seg.event)) {
- _this.segDragMousedown(seg, ev);
- }
- }
- },
- function(name, func) {
- // attach the handler to the container element and only listen for real event elements via bubbling
- _this.el.on(name, '.fc-event-container > *', function(ev) {
- var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
-
- // only call the handlers if there is not a drag/resize in progress
- if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
- return func.call(this, seg, ev); // `this` will be the event element
- }
- });
+ this.el.on(name, '.fc-event-container > *', function(ev) {
+ var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
+
+ // only call the handlers if there is not a drag/resize in progress
+ if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
+ return handler.call(_this, seg, ev); // context will be the Grid
}
- );
+ });
+ },
+
+
+ handleSegClick: function(seg, ev) {
+ return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
},
// Updates internal state and triggers handlers for when an event element is moused over
- triggerSegMouseover: function(seg, ev) {
- if (!this.mousedOverSeg) {
+ handleSegMouseover: function(seg, ev) {
+ if (
+ !this.isIgnoringMouse &&
+ !this.mousedOverSeg
+ ) {
this.mousedOverSeg = seg;
+ seg.el.addClass('fc-allow-mouse-resize');
this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
}
},
@@ -3817,56 +4236,132 @@ Grid.mixin({
// Updates internal state and triggers handlers for when an event element is moused out.
// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
- triggerSegMouseout: function(seg, ev) {
+ handleSegMouseout: function(seg, ev) {
ev = ev || {}; // if given no args, make a mock mouse event
if (this.mousedOverSeg) {
seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
this.mousedOverSeg = null;
+ seg.el.removeClass('fc-allow-mouse-resize');
this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
}
},
+ handleSegMousedown: function(seg, ev) {
+ var isResizing = this.startSegResize(seg, ev, { distance: 5 });
+
+ if (!isResizing && this.view.isEventDraggable(seg.event)) {
+ this.buildSegDragListener(seg)
+ .startInteraction(ev, {
+ distance: 5
+ });
+ }
+ },
+
+
+ handleSegTouchStart: function(seg, ev) {
+ var view = this.view;
+ var event = seg.event;
+ var isSelected = view.isEventSelected(event);
+ var isDraggable = view.isEventDraggable(event);
+ var isResizable = view.isEventResizable(event);
+ var isResizing = false;
+ var dragListener;
+
+ if (isSelected && isResizable) {
+ // only allow resizing of the event is selected
+ isResizing = this.startSegResize(seg, ev);
+ }
+
+ if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
+
+ dragListener = isDraggable ?
+ this.buildSegDragListener(seg) :
+ this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
+
+ dragListener.startInteraction(ev, { // won't start if already started
+ delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
+ });
+ }
+
+ // a long tap simulates a mouseover. ignore this bogus mouseover.
+ this.tempIgnoreMouse();
+ },
+
+
+ handleSegTouchEnd: function(seg, ev) {
+ // touchstart+touchend = click, which simulates a mouseover.
+ // ignore this bogus mouseover.
+ this.tempIgnoreMouse();
+ },
+
+
+ // returns boolean whether resizing actually started or not.
+ // assumes the seg allows resizing.
+ // `dragOptions` are optional.
+ startSegResize: function(seg, ev, dragOptions) {
+ if ($(ev.target).is('.fc-resizer')) {
+ this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
+ .startInteraction(ev, dragOptions);
+ return true;
+ }
+ return false;
+ },
+
+
+
/* Event Dragging
------------------------------------------------------------------------------------------------------------------*/
- // Called when the user does a mousedown on an event, which might lead to dragging.
+ // Builds a listener that will track user-dragging on an event segment.
// Generic enough to work with any type of Grid.
- segDragMousedown: function(seg, ev) {
+ // Has side effect of setting/unsetting `segDragListener`
+ buildSegDragListener: function(seg) {
var _this = this;
var view = this.view;
var calendar = view.calendar;
var el = seg.el;
var event = seg.event;
+ var isDragging;
+ var mouseFollower; // A clone of the original element that will move with the mouse
var dropLocation; // zoned event date properties
- // A clone of the original element that will move with the mouse
- var mouseFollower = new MouseFollower(seg.el, {
- parentEl: view.el,
- opacity: view.opt('dragOpacity'),
- revertDuration: view.opt('dragRevertDuration'),
- zIndex: 2 // one above the .fc-view
- });
+ if (this.segDragListener) {
+ return this.segDragListener;
+ }
// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
// of the view.
- var dragListener = new HitDragListener(view, {
- distance: 5,
+ var dragListener = this.segDragListener = new HitDragListener(view, {
scroll: view.opt('dragScroll'),
subjectEl: el,
subjectCenter: true,
- listenStart: function(ev) {
+ interactionStart: function(ev) {
+ isDragging = false;
+ mouseFollower = new MouseFollower(seg.el, {
+ additionalClass: 'fc-dragging',
+ parentEl: view.el,
+ opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
+ revertDuration: view.opt('dragRevertDuration'),
+ zIndex: 2 // one above the .fc-view
+ });
mouseFollower.hide(); // don't show until we know this is a real drag
mouseFollower.start(ev);
},
dragStart: function(ev) {
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
+ if (dragListener.isTouch && !view.isEventSelected(event)) {
+ // if not previously selected, will fire after a delay. then, select the event
+ view.selectEvent(event);
+ }
+ isDragging = true;
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
_this.segDragStart(seg, ev);
view.hideEvent(event); // hide all event segments. our mouseFollower will take over
},
hitOver: function(hit, isOrig, origHit) {
+ var dragHelperEls;
// starting hit could be forced (DayGrid.limit)
if (seg.hit) {
@@ -3886,7 +4381,13 @@ Grid.mixin({
}
// if a valid drop location, have the subclass render a visual indication
- if (dropLocation && view.renderDrag(dropLocation, seg)) {
+ if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
+
+ dragHelperEls.addClass('fc-dragging');
+ if (!dragListener.isTouch) {
+ _this.applyDragOpacity(dragHelperEls);
+ }
+
mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
}
else {
@@ -3902,27 +4403,54 @@ Grid.mixin({
mouseFollower.show(); // show in case we are moving out of all hits
dropLocation = null;
},
- hitDone: function() { // Called after a hitOut OR before a dragStop
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
enableCursor();
},
- dragStop: function(ev) {
+ interactionEnd: function(ev) {
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
mouseFollower.stop(!dropLocation, function() {
- view.unrenderDrag();
- view.showEvent(event);
- _this.segDragStop(seg, ev);
-
+ if (isDragging) {
+ view.unrenderDrag();
+ view.showEvent(event);
+ _this.segDragStop(seg, ev);
+ }
if (dropLocation) {
view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
}
});
+ _this.segDragListener = null;
+ }
+ });
+
+ return dragListener;
+ },
+
+
+ // seg isn't draggable, but let's use a generic DragListener
+ // simply for the delay, so it can be selected.
+ // Has side effect of setting/unsetting `segDragListener`
+ buildSegSelectListener: function(seg) {
+ var _this = this;
+ var view = this.view;
+ var event = seg.event;
+
+ if (this.segDragListener) {
+ return this.segDragListener;
+ }
+
+ var dragListener = this.segDragListener = new DragListener({
+ dragStart: function(ev) {
+ if (dragListener.isTouch && !view.isEventSelected(event)) {
+ // if not previously selected, will fire after a delay. then, select the event
+ view.selectEvent(event);
+ }
},
- listenStop: function() {
- mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
+ interactionEnd: function(ev) {
+ _this.segDragListener = null;
}
});
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
+ return dragListener;
},
@@ -4038,8 +4566,8 @@ Grid.mixin({
var dropLocation; // a null value signals an unsuccessful drag
// listener that tracks mouse movement over date-associated pixel regions
- var dragListener = new HitDragListener(this, {
- listenStart: function() {
+ var dragListener = _this.externalDragListener = new HitDragListener(this, {
+ interactionStart: function() {
_this.isDraggingExternal = true;
},
hitOver: function(hit) {
@@ -4063,17 +4591,16 @@ Grid.mixin({
hitOut: function() {
dropLocation = null; // signal unsuccessful
},
- hitDone: function() { // Called after a hitOut OR before a dragStop
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
enableCursor();
_this.unrenderDrag();
},
- dragStop: function() {
+ interactionEnd: function(ev) {
if (dropLocation) { // element was dropped on a valid hit
_this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
}
- },
- listenStop: function() {
_this.isDraggingExternal = false;
+ _this.externalDragListener = null;
}
});
@@ -4114,6 +4641,7 @@ Grid.mixin({
// `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
// `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
// A truthy returned value indicates this method has rendered a helper element.
+ // Must return elements used for any mock events.
renderDrag: function(dropLocation, seg) {
// subclasses must implement
},
@@ -4129,24 +4657,28 @@ Grid.mixin({
------------------------------------------------------------------------------------------------------------------*/
- // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
+ // Creates a listener that tracks the user as they resize an event segment.
// Generic enough to work with any type of Grid.
- segResizeMousedown: function(seg, ev, isStart) {
+ buildSegResizeListener: function(seg, isStart) {
var _this = this;
var view = this.view;
var calendar = view.calendar;
var el = seg.el;
var event = seg.event;
var eventEnd = calendar.getEventEnd(event);
+ var isDragging;
var resizeLocation; // zoned event date properties. falsy if invalid resize
// Tracks mouse movement over the *grid's* coordinate map
- var dragListener = new HitDragListener(this, {
- distance: 5,
+ var dragListener = this.segResizeListener = new HitDragListener(this, {
scroll: view.opt('dragScroll'),
subjectEl: el,
+ interactionStart: function() {
+ isDragging = false;
+ },
dragStart: function(ev) {
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
+ isDragging = true;
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
_this.segResizeStart(seg, ev);
},
hitOver: function(hit, isOrig, origHit) {
@@ -4181,16 +4713,18 @@ Grid.mixin({
view.showEvent(event);
enableCursor();
},
- dragStop: function(ev) {
- _this.segResizeStop(seg, ev);
-
+ interactionEnd: function(ev) {
+ if (isDragging) {
+ _this.segResizeStop(seg, ev);
+ }
if (resizeLocation) { // valid date to resize to?
view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
}
+ _this.segResizeListener = null;
}
});
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
+ return dragListener;
},
@@ -4267,6 +4801,7 @@ Grid.mixin({
// Renders a visual indication of an event being resized.
// `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
+ // Must return elements used for any mock events.
renderEventResize: function(range, seg) {
// subclasses must implement
},
@@ -4312,6 +4847,7 @@ Grid.mixin({
// Generic utility for generating the HTML classNames for an event segment's element
getSegClasses: function(seg, isDraggable, isResizable) {
+ var view = this.view;
var event = seg.event;
var classes = [
'fc-event',
@@ -4329,6 +4865,11 @@ Grid.mixin({
classes.push('fc-resizable');
}
+ // event is currently selected? attach a className.
+ if (view.isEventSelected(event)) {
+ classes.push('fc-selected');
+ }
+
return classes;
},
@@ -5311,10 +5852,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
// if a segment from the same calendar but another component is being dragged, render a helper event
if (seg && !seg.el.closest(this.el).length) {
- this.renderEventLocationHelper(eventLocation, seg);
- this.applyDragOpacity(this.helperEls);
-
- return true; // a helper has been rendered
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
}
},
@@ -5333,7 +5871,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
// Renders a visual indication of an event being resized
renderEventResize: function(eventLocation, seg) {
this.renderHighlight(this.eventToSpan(eventLocation));
- this.renderEventLocationHelper(eventLocation, seg);
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
},
@@ -5379,7 +5917,9 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
helperNodes.push(skeletonEl[0]);
});
- this.helperEls = $(helperNodes); // array -> jQuery set
+ return ( // must return the elements rendered
+ this.helperEls = $(helperNodes) // array -> jQuery set
+ );
},
@@ -6165,6 +6705,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
labelInterval: null, // duration of how often a label should be displayed for a slot
colEls: null, // cells elements in the day-row background
+ slatContainerEl: null, // div that wraps all the slat rows
slatEls: null, // elements running horizontally across all columns
nowIndicatorEls: null,
@@ -6184,7 +6725,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
renderDates: function() {
this.el.html(this.renderHtml());
this.colEls = this.el.find('.fc-day');
- this.slatEls = this.el.find('.fc-slats tr');
+ this.slatContainerEl = this.el.find('.fc-slats');
+ this.slatEls = this.slatContainerEl.find('tr');
this.colCoordCache = new CoordCache({
els: this.colEls,
@@ -6463,6 +7005,11 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
},
+ getTotalSlatHeight: function() {
+ return this.slatContainerEl.outerHeight();
+ },
+
+
// Computes the top coordinate, relative to the bounds of the grid, of the given date.
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
computeDateTop: function(date, startOfDayDate) {
@@ -6511,13 +7058,10 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
renderDrag: function(eventLocation, seg) {
if (seg) { // if there is event information for this drag, render a helper event
- this.renderEventLocationHelper(eventLocation, seg);
- for (var i = 0; i < this.helperSegs.length; i++) {
- this.applyDragOpacity(this.helperSegs[i].el);
- }
-
- return true; // signal that a helper has been rendered
+ // returns mock event elements
+ // signal that a helper has been rendered
+ return this.renderEventLocationHelper(eventLocation, seg);
}
else {
// otherwise, just render a highlight
@@ -6539,7 +7083,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
// Renders a visual indication of an event being resized
renderEventResize: function(eventLocation, seg) {
- this.renderEventLocationHelper(eventLocation, seg);
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
},
@@ -6555,7 +7099,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
renderHelper: function(event, sourceSeg) {
- this.renderHelperSegs(this.eventToSegs(event), sourceSeg);
+ return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
},
@@ -6749,6 +7293,7 @@ TimeGrid.mixin({
renderHelperSegs: function(segs, sourceSeg) {
+ var helperEls = [];
var i, seg;
var sourceEl;
@@ -6766,9 +7311,12 @@ TimeGrid.mixin({
'margin-right': sourceEl.css('margin-right')
});
}
+ helperEls.push(seg.el[0]);
}
this.helperSegs = segs;
+
+ return $(helperEls); // must return rendered helpers
},
@@ -7279,7 +7827,7 @@ function isSlotSegCollision(seg1, seg2) {
/* An abstract class from which other views inherit from
----------------------------------------------------------------------------------------------------------------------*/
-var View = FC.View = Class.extend({
+var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
type: null, // subclass' view name (string)
name: null, // deprecated. use `type` instead
@@ -7306,13 +7854,10 @@ var View = FC.View = Class.extend({
isRTL: false,
isSelected: false, // boolean whether a range of time is user-selected or not
+ selectedEvent: null,
eventOrderSpecs: null, // criteria for ordering events when they have same date/time
- // subclasses can optionally use a scroll container
- scrollerEl: null, // the element that will most likely scroll when content is too tall
- scrollTop: null, // cached vertical scroll value
-
// classNames styled by jqui themes
widgetHeaderClass: null,
widgetContentClass: null,
@@ -7322,9 +7867,6 @@ var View = FC.View = Class.extend({
nextDayThreshold: null,
isHiddenDayHash: null,
- // document handlers, bound to `this` object
- documentMousedownProxy: null, // TODO: doesn't work with touch
-
// now indicator
isNowIndicatorRendered: null,
initialNowDate: null, // result first getNow call
@@ -7347,8 +7889,6 @@ var View = FC.View = Class.extend({
this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
- this.documentMousedownProxy = proxy(this, 'documentMousedown');
-
this.initialize();
},
@@ -7674,13 +8214,14 @@ var View = FC.View = Class.extend({
// Binds DOM handlers to elements that reside outside the view container, such as the document
bindGlobalHandlers: function() {
- $(document).on('mousedown', this.documentMousedownProxy);
+ this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
+ this.listenTo($(document), 'touchstart', this.processUnselect);
},
// Unbinds DOM handlers from elements that reside outside the view container
unbindGlobalHandlers: function() {
- $(document).off('mousedown', this.documentMousedownProxy);
+ this.stopListeningTo($(document));
},
@@ -7848,27 +8389,6 @@ var View = FC.View = Class.extend({
------------------------------------------------------------------------------------------------------------------*/
- // Given the total height of the view, return the number of pixels that should be used for the scroller.
- // Utility for subclasses.
- computeScrollerHeight: function(totalHeight) {
- var scrollerEl = this.scrollerEl;
- var both;
- var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
-
- both = this.el.add(scrollerEl);
-
- // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
- both.css({
- position: 'relative', // cause a reflow, which will force fresh dimension recalculation
- left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
- });
- otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
- both.css({ position: '', left: '' }); // undo hack
-
- return totalHeight - otherHeight;
- },
-
-
// Computes the initial pre-configured scroll state prior to allowing the user to change it.
// Given the scroll state from the previous rendering. If first time rendering, given null.
computeInitialScroll: function(previousScrollState) {
@@ -7878,17 +8398,13 @@ var View = FC.View = Class.extend({
// Retrieves the view's current natural scroll state. Can return an arbitrary format.
queryScroll: function() {
- if (this.scrollerEl) {
- return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
- }
+ // subclasses must implement
},
// Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
setScroll: function(scrollState) {
- if (this.scrollerEl) {
- return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
- }
+ // subclasses must implement
},
@@ -8103,7 +8619,8 @@ var View = FC.View = Class.extend({
// Renders a visual indication of a event or external-element drag over the given drop zone.
- // If an external-element, seg will be `null`
+ // If an external-element, seg will be `null`.
+ // Must return elements used for any mock events.
renderDrag: function(dropLocation, seg) {
// subclasses must implement
},
@@ -8166,7 +8683,7 @@ var View = FC.View = Class.extend({
},
- /* Selection
+ /* Selection (time range)
------------------------------------------------------------------------------------------------------------------*/
@@ -8224,13 +8741,62 @@ var View = FC.View = Class.extend({
},
- // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
- documentMousedown: function(ev) {
- var ignore;
+ /* Event Selection
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ selectEvent: function(event) {
+ if (!this.selectedEvent || this.selectedEvent !== event) {
+ this.unselectEvent();
+ this.renderedEventSegEach(function(seg) {
+ seg.el.addClass('fc-selected');
+ }, event);
+ this.selectedEvent = event;
+ }
+ },
+
+
+ unselectEvent: function() {
+ if (this.selectedEvent) {
+ this.renderedEventSegEach(function(seg) {
+ seg.el.removeClass('fc-selected');
+ }, this.selectedEvent);
+ this.selectedEvent = null;
+ }
+ },
- // is there a selection, and has the user made a proper left click?
- if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
+ isEventSelected: function(event) {
+ // event references might change on refetchEvents(), while selectedEvent doesn't,
+ // so compare IDs
+ return this.selectedEvent && this.selectedEvent._id === event._id;
+ },
+
+
+ /* Mouse / Touch Unselecting (time range & event unselection)
+ ------------------------------------------------------------------------------------------------------------------*/
+ // TODO: move consistently to down/start or up/end?
+ // TODO: don't kill previous selection if touch scrolling
+
+
+ handleDocumentMousedown: function(ev) {
+ if (isPrimaryMouseButton(ev)) {
+ this.processUnselect(ev);
+ }
+ },
+
+
+ processUnselect: function(ev) {
+ this.processRangeUnselect(ev);
+ this.processEventUnselect(ev);
+ },
+
+
+ processRangeUnselect: function(ev) {
+ var ignore;
+
+ // is there a time-range selection?
+ if (this.isSelected && this.opt('unselectAuto')) {
// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
ignore = this.opt('unselectCancel');
if (!ignore || !$(ev.target).closest(ignore).length) {
@@ -8240,6 +8806,15 @@ var View = FC.View = Class.extend({
},
+ processEventUnselect: function(ev) {
+ if (this.selectedEvent) {
+ if (!$(ev.target).closest('.fc-selected').length) {
+ this.unselectEvent();
+ }
+ }
+ },
+
+
/* Day Click
------------------------------------------------------------------------------------------------------------------*/
@@ -8354,6 +8929,127 @@ var View = FC.View = Class.extend({
;;
+/*
+Embodies a div that has potential scrollbars
+*/
+var Scroller = FC.Scroller = Class.extend({
+
+ el: null, // the guaranteed outer element
+ scrollEl: null, // the element with the scrollbars
+ overflowX: null,
+ overflowY: null,
+
+
+ constructor: function(options) {
+ options = options || {};
+ this.overflowX = options.overflowX || options.overflow || 'auto';
+ this.overflowY = options.overflowY || options.overflow || 'auto';
+ },
+
+
+ render: function() {
+ this.el = this.renderEl();
+ this.applyOverflow();
+ },
+
+
+ renderEl: function() {
+ return (this.scrollEl = $('<div class="fc-scroller"></div>'));
+ },
+
+
+ // sets to natural height, unlocks overflow
+ clear: function() {
+ this.setHeight('auto');
+ this.applyOverflow();
+ },
+
+
+ destroy: function() {
+ this.el.remove();
+ },
+
+
+ // Overflow
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ applyOverflow: function() {
+ this.scrollEl.css({
+ 'overflow-x': this.overflowX,
+ 'overflow-y': this.overflowY
+ });
+ },
+
+
+ // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
+ // Useful for preserving scrollbar widths regardless of future resizes.
+ // Can pass in scrollbarWidths for optimization.
+ lockOverflow: function(scrollbarWidths) {
+ var overflowX = this.overflowX;
+ var overflowY = this.overflowY;
+
+ scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
+
+ if (overflowX === 'auto') {
+ overflowX = (
+ scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
+ // OR scrolling pane with massless scrollbars?
+ this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
+ // subtract 1 because of IE off-by-one issue
+ ) ? 'scroll' : 'hidden';
+ }
+
+ if (overflowY === 'auto') {
+ overflowY = (
+ scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
+ // OR scrolling pane with massless scrollbars?
+ this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
+ // subtract 1 because of IE off-by-one issue
+ ) ? 'scroll' : 'hidden';
+ }
+
+ this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
+ },
+
+
+ // Getters / Setters
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ setHeight: function(height) {
+ this.scrollEl.height(height);
+ },
+
+
+ getScrollTop: function() {
+ return this.scrollEl.scrollTop();
+ },
+
+
+ setScrollTop: function(top) {
+ this.scrollEl.scrollTop(top);
+ },
+
+
+ getClientWidth: function() {
+ return this.scrollEl[0].clientWidth;
+ },
+
+
+ getClientHeight: function() {
+ return this.scrollEl[0].clientHeight;
+ },
+
+
+ getScrollbarWidths: function() {
+ return getScrollbarWidths(this.scrollEl);
+ }
+
+});
+
+;;
+
var Calendar = FC.Calendar = Class.extend({
dirDefaults: null, // option defaults related to LTR or RTL
@@ -8608,7 +9304,7 @@ var Calendar = FC.Calendar = Class.extend({
});
-Calendar.mixin(Emitter);
+Calendar.mixin(EmitterMixin);
function Calendar_constructor(element, overrides) {
@@ -9369,7 +10065,9 @@ Calendar.defaults = {
dayPopoverFormat: 'LL',
handleWindowResize: true,
- windowResizeDelay: 200 // milliseconds before an updateSize happens
+ windowResizeDelay: 200, // milliseconds before an updateSize happens
+
+ longPressDelay: 1000
};
@@ -10368,6 +11066,8 @@ function EventManager(options) { // assumed to be a calendar
assignDatesToEvent(start, end, allDay, out);
}
+ t.normalizeEvent(out); // hook for external use. a prototype method
+
return out;
}
@@ -10900,6 +11600,12 @@ function EventManager(options) { // assumed to be a calendar
}
+// hook for external libs to manipulate event properties upon creation.
+// should manipulate the event in-place.
+Calendar.prototype.normalizeEvent = function(event) {
+};
+
+
// Returns a list of events that the given event should be compared against when being considered for a move to
// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
Calendar.prototype.getPeerEvents = function(span, event) {
@@ -10937,6 +11643,8 @@ function backupEventDates(event) {
var BasicView = FC.BasicView = View.extend({
+ scroller: null,
+
dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
dayGrid: null, // the main subcomponent that does most of the heavy lifting
@@ -10951,6 +11659,11 @@ var BasicView = FC.BasicView = View.extend({
initialize: function() {
this.dayGrid = this.instantiateDayGrid();
+
+ this.scroller = new Scroller({
+ overflowX: 'hidden',
+ overflowY: 'auto'
+ });
},
@@ -11003,9 +11716,12 @@ var BasicView = FC.BasicView = View.extend({
this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
this.renderHead();
- this.scrollerEl = this.el.find('.fc-day-grid-container');
+ this.scroller.render();
+ var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
+ var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
+ this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
- this.dayGrid.setElement(this.el.find('.fc-day-grid'));
+ this.dayGrid.setElement(dayGridEl);
this.dayGrid.renderDates(this.hasRigidRows());
},
@@ -11024,6 +11740,7 @@ var BasicView = FC.BasicView = View.extend({
unrenderDates: function() {
this.dayGrid.unrenderDates();
this.dayGrid.removeElement();
+ this.scroller.destroy();
},
@@ -11044,11 +11761,7 @@ var BasicView = FC.BasicView = View.extend({
'</thead>' +
'<tbody class="fc-body">' +
'<tr>' +
- '<td class="' + this.widgetContentClass + '">' +
- '<div class="fc-day-grid-container">' +
- '<div class="fc-day-grid"/>' +
- '</div>' +
- '</td>' +
+ '<td class="' + this.widgetContentClass + '"></td>' +
'</tr>' +
'</tbody>' +
'</table>';
@@ -11091,9 +11804,10 @@ var BasicView = FC.BasicView = View.extend({
setHeight: function(totalHeight, isAuto) {
var eventLimit = this.opt('eventLimit');
var scrollerHeight;
+ var scrollbarWidths;
// reset all heights to be natural
- unsetScroller(this.scrollerEl);
+ this.scroller.clear();
uncompensateScroll(this.headRowEl);
this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
@@ -11103,6 +11817,8 @@ var BasicView = FC.BasicView = View.extend({
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
}
+ // distribute the height to the rows
+ // (totalHeight is a "recommended" value if isAuto)
scrollerHeight = this.computeScrollerHeight(totalHeight);
this.setGridHeight(scrollerHeight, isAuto);
@@ -11111,17 +11827,33 @@ var BasicView = FC.BasicView = View.extend({
this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
}
- if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
+ if (!isAuto) { // should we force dimensions of the scroll container?
- compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
+ this.scroller.setHeight(scrollerHeight);
+ scrollbarWidths = this.scroller.getScrollbarWidths();
- // doing the scrollbar compensation might have created text overflow which created more height. redo
- scrollerHeight = this.computeScrollerHeight(totalHeight);
- this.scrollerEl.height(scrollerHeight);
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
+
+ compensateScroll(this.headRowEl, scrollbarWidths);
+
+ // doing the scrollbar compensation might have created text overflow which created more height. redo
+ scrollerHeight = this.computeScrollerHeight(totalHeight);
+ this.scroller.setHeight(scrollerHeight);
+ }
+
+ // guarantees the same scrollbar widths
+ this.scroller.lockOverflow(scrollbarWidths);
}
},
+ // given a desired total height of the view, returns what the height of the scroller should be
+ computeScrollerHeight: function(totalHeight) {
+ return totalHeight -
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
+ },
+
+
// Sets the height of just the DayGrid component in this view
setGridHeight: function(height, isAuto) {
if (isAuto) {
@@ -11133,6 +11865,20 @@ var BasicView = FC.BasicView = View.extend({
},
+ /* Scroll
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ queryScroll: function() {
+ return this.scroller.getScrollTop();
+ },
+
+
+ setScroll: function(top) {
+ this.scroller.setScrollTop(top);
+ },
+
+
/* Hit Areas
------------------------------------------------------------------------------------------------------------------*/
// forward all hit-related method calls to dayGrid
@@ -11368,6 +12114,8 @@ fcViews.month = {
var AgendaView = FC.AgendaView = View.extend({
+ scroller: null,
+
timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
timeGrid: null, // the main time-grid subcomponent of this view
@@ -11377,11 +12125,10 @@ var AgendaView = FC.AgendaView = View.extend({
axisWidth: null, // the width of the time axis running down the side
headContainerEl: null, // div that hold's the timeGrid's rendered date header
- noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
+ noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
bottomRuleEl: null,
- bottomRuleHeight: null,
initialize: function() {
@@ -11390,6 +12137,11 @@ var AgendaView = FC.AgendaView = View.extend({
if (this.opt('allDaySlot')) { // should we display the "all-day" area?
this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
}
+
+ this.scroller = new Scroller({
+ overflowX: 'hidden',
+ overflowY: 'auto'
+ });
},
@@ -11430,10 +12182,12 @@ var AgendaView = FC.AgendaView = View.extend({
this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
this.renderHead();
- // the element that wraps the time-grid that will probably scroll
- this.scrollerEl = this.el.find('.fc-time-grid-container');
+ this.scroller.render();
+ var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
+ var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
+ this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
- this.timeGrid.setElement(this.el.find('.fc-time-grid'));
+ this.timeGrid.setElement(timeGridEl);
this.timeGrid.renderDates();
// the <hr> that sometimes displays under the time-grid
@@ -11470,6 +12224,8 @@ var AgendaView = FC.AgendaView = View.extend({
this.dayGrid.unrenderDates();
this.dayGrid.removeElement();
}
+
+ this.scroller.destroy();
},
@@ -11491,9 +12247,6 @@ var AgendaView = FC.AgendaView = View.extend({
'<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
''
) +
- '<div class="fc-time-grid-container">' +
- '<div class="fc-time-grid"/>' +
- '</div>' +
'</td>' +
'</tr>' +
'</tbody>' +
@@ -11573,16 +12326,11 @@ var AgendaView = FC.AgendaView = View.extend({
setHeight: function(totalHeight, isAuto) {
var eventLimit;
var scrollerHeight;
-
- if (this.bottomRuleHeight === null) {
- // calculate the height of the rule the very first time
- this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
- }
- this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
+ var scrollbarWidths;
// reset all dimensions back to the original state
- this.scrollerEl.css('overflow', '');
- unsetScroller(this.scrollerEl);
+ this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
+ this.scroller.clear(); // sets height to 'auto' and clears overflow
uncompensateScroll(this.noScrollRowEls);
// limit number of events in the all-day area
@@ -11598,28 +12346,46 @@ var AgendaView = FC.AgendaView = View.extend({
}
}
- if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
+ if (!isAuto) { // should we force dimensions of the scroll container?
scrollerHeight = this.computeScrollerHeight(totalHeight);
- if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
+ this.scroller.setHeight(scrollerHeight);
+ scrollbarWidths = this.scroller.getScrollbarWidths();
+
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
// make the all-day and header rows lines up
- compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
+ compensateScroll(this.noScrollRowEls, scrollbarWidths);
// the scrollbar compensation might have changed text flow, which might affect height, so recalculate
// and reapply the desired height to the scroller.
scrollerHeight = this.computeScrollerHeight(totalHeight);
- this.scrollerEl.height(scrollerHeight);
+ this.scroller.setHeight(scrollerHeight);
}
- else { // no scrollbars
- // still, force a height and display the bottom rule (marks the end of day)
- this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
+
+ // guarantees the same scrollbar widths
+ this.scroller.lockOverflow(scrollbarWidths);
+
+ // if there's any space below the slats, show the horizontal rule.
+ // this won't cause any new overflow, because lockOverflow already called.
+ if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
this.bottomRuleEl.show();
}
}
},
+ // given a desired total height of the view, returns what the height of the scroller should be
+ computeScrollerHeight: function(totalHeight) {
+ return totalHeight -
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
+ },
+
+
+ /* Scroll
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
// Computes the initial pre-configured scroll state prior to allowing the user to change it
computeInitialScroll: function() {
var scrollTime = moment.duration(this.opt('scrollTime'));
@@ -11636,6 +12402,16 @@ var AgendaView = FC.AgendaView = View.extend({
},
+ queryScroll: function() {
+ return this.scroller.getScrollTop();
+ },
+
+
+ setScroll: function(top) {
+ this.scroller.setScrollTop(top);
+ },
+
+
/* Hit Areas
------------------------------------------------------------------------------------------------------------------*/
// forward all hit-related method calls to the grids (dayGrid might not be defined)