3 * Docs & License: https://fullcalendar.io/
8 if (typeof define
=== 'function' && define
.amd
) {
9 define([ 'jquery', 'moment' ], factory
);
11 else if (typeof exports
=== 'object') { // Node/CommonJS
12 module
.exports
= factory(require('jquery'), require('moment'));
15 factory(jQuery
, moment
);
17 })(function($, moment
) {
21 var FC
= $.fullCalendar
= {
23 // When introducing internal API incompatibilities (where fullcalendar plugins would break),
24 // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
25 // and the below integer should be incremented.
26 internalApiVersion
: 10
28 var fcViews
= FC
.views
= {};
31 $.fn
.fullCalendar = function(options
) {
32 var args
= Array
.prototype.slice
.call(arguments
, 1); // for a possible method call
33 var res
= this; // what this function will return (this jQuery object by default)
35 this.each(function(i
, _element
) { // loop each DOM element involved
36 var element
= $(_element
);
37 var calendar
= element
.data('fullCalendar'); // get the existing calendar object (if any)
38 var singleRes
; // the returned value of this single method call
41 if (typeof options
=== 'string') {
43 if (options
=== 'getCalendar') {
44 if (!i
) { // first element only
48 else if (options
=== 'destroy') { // don't warn if no calendar object
51 element
.removeData('fullCalendar');
55 FC
.warn("Attempting to call a FullCalendar method on an element with no calendar.");
57 else if ($.isFunction(calendar
[options
])) {
58 singleRes
= calendar
[options
].apply(calendar
, args
);
61 res
= singleRes
; // record the first method call result
63 if (options
=== 'destroy') { // for the destroy method, must remove Calendar object data
64 element
.removeData('fullCalendar');
68 FC
.warn("'" + options
+ "' is an unknown FullCalendar method.");
71 // a new calendar initialization
72 else if (!calendar
) { // don't initialize twice
73 calendar
= new Calendar(element
, options
);
74 element
.data('fullCalendar', calendar
);
83 var complexOptions
= [ // names of options that are objects whose properties should be combined
92 // Merges an array of option objects into a single object
93 function mergeOptions(optionObjs
) {
94 return mergeProps(optionObjs
, complexOptions
);
100 FC
.applyAll
= applyAll
;
101 FC
.debounce
= debounce
;
103 FC
.htmlEscape
= htmlEscape
;
104 FC
.cssToStr
= cssToStr
;
106 FC
.capitaliseFirstLetter
= capitaliseFirstLetter
;
109 /* FullCalendar-specific DOM Utilities
110 ----------------------------------------------------------------------------------------------------------------------*/
113 // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
114 // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
115 function compensateScroll(rowEls
, scrollbarWidths
) {
116 if (scrollbarWidths
.left
) {
118 'border-left-width': 1,
119 'margin-left': scrollbarWidths
.left
- 1
122 if (scrollbarWidths
.right
) {
124 'border-right-width': 1,
125 'margin-right': scrollbarWidths
.right
- 1
131 // Undoes compensateScroll and restores all borders/margins
132 function uncompensateScroll(rowEls
) {
136 'border-left-width': '',
137 'border-right-width': ''
142 // Make the mouse cursor express that an event is not allowed in the current area
143 function disableCursor() {
144 $('body').addClass('fc-not-allowed');
148 // Returns the mouse cursor to its original look
149 function enableCursor() {
150 $('body').removeClass('fc-not-allowed');
154 // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
155 // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
156 // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
157 // reduces the available height.
158 function distributeHeight(els
, availableHeight
, shouldRedistribute
) {
160 // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
161 // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
163 var minOffset1
= Math
.floor(availableHeight
/ els
.length
); // for non-last element
164 var minOffset2
= Math
.floor(availableHeight
- minOffset1
* (els
.length
- 1)); // for last element *FLOORING NOTE*
165 var flexEls
= []; // elements that are allowed to expand. array of DOM nodes
166 var flexOffsets
= []; // amount of vertical space it takes up
167 var flexHeights
= []; // actual css height
170 undistributeHeight(els
); // give all elements their natural height
172 // find elements that are below the recommended height (expandable).
173 // important to query for heights in a single first pass (to avoid reflow oscillation).
174 els
.each(function(i
, el
) {
175 var minOffset
= i
=== els
.length
- 1 ? minOffset2
: minOffset1
;
176 var naturalOffset
= $(el
).outerHeight(true);
178 if (naturalOffset
< minOffset
) {
180 flexOffsets
.push(naturalOffset
);
181 flexHeights
.push($(el
).height());
184 // this element stretches past recommended height (non-expandable). mark the space as occupied.
185 usedHeight
+= naturalOffset
;
189 // readjust the recommended height to only consider the height available to non-maxed-out rows.
190 if (shouldRedistribute
) {
191 availableHeight
-= usedHeight
;
192 minOffset1
= Math
.floor(availableHeight
/ flexEls
.length
);
193 minOffset2
= Math
.floor(availableHeight
- minOffset1
* (flexEls
.length
- 1)); // *FLOORING NOTE*
196 // assign heights to all expandable elements
197 $(flexEls
).each(function(i
, el
) {
198 var minOffset
= i
=== flexEls
.length
- 1 ? minOffset2
: minOffset1
;
199 var naturalOffset
= flexOffsets
[i
];
200 var naturalHeight
= flexHeights
[i
];
201 var newHeight
= minOffset
- (naturalOffset
- naturalHeight
); // subtract the margin/padding
203 if (naturalOffset
< minOffset
) { // we check this again because redistribution might have changed things
204 $(el
).height(newHeight
);
210 // Undoes distrubuteHeight, restoring all els to their natural height
211 function undistributeHeight(els
) {
216 // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
217 // cells to be that width.
218 // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
219 function matchCellWidths(els
) {
220 var maxInnerWidth
= 0;
222 els
.find('> *').each(function(i
, innerEl
) {
223 var innerWidth
= $(innerEl
).outerWidth();
224 if (innerWidth
> maxInnerWidth
) {
225 maxInnerWidth
= innerWidth
;
229 maxInnerWidth
++; // sometimes not accurate of width the text needs to stay on one line. insurance
231 els
.width(maxInnerWidth
);
233 return maxInnerWidth
;
237 // Given one element that resides inside another,
238 // Subtracts the height of the inner element from the outer element.
239 function subtractInnerElHeight(outerEl
, innerEl
) {
240 var both
= outerEl
.add(innerEl
);
243 // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
245 position
: 'relative', // cause a reflow, which will force fresh dimension recalculation
246 left
: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
248 diff
= outerEl
.outerHeight() - innerEl
.outerHeight(); // grab the dimensions
249 both
.css({ position
: '', left
: '' }); // undo hack
255 /* Element Geom Utilities
256 ----------------------------------------------------------------------------------------------------------------------*/
258 FC
.getOuterRect
= getOuterRect
;
259 FC
.getClientRect
= getClientRect
;
260 FC
.getContentRect
= getContentRect
;
261 FC
.getScrollbarWidths
= getScrollbarWidths
;
264 // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
265 function getScrollParent(el
) {
266 var position
= el
.css('position'),
267 scrollParent
= el
.parents().filter(function() {
268 var parent
= $(this);
269 return (/(auto|scroll)/).test(
270 parent
.css('overflow') + parent
.css('overflow-y') + parent
.css('overflow-x')
274 return position
=== 'fixed' || !scrollParent
.length
? $(el
[0].ownerDocument
|| document
) : scrollParent
;
278 // Queries the outer bounding area of a jQuery element.
279 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
280 // Origin is optional.
281 function getOuterRect(el
, origin
) {
282 var offset
= el
.offset();
283 var left
= offset
.left
- (origin
? origin
.left
: 0);
284 var top
= offset
.top
- (origin
? origin
.top
: 0);
288 right
: left
+ el
.outerWidth(),
290 bottom
: top
+ el
.outerHeight()
295 // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
296 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
297 // Origin is optional.
298 // WARNING: given element can't have borders
299 // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
300 function getClientRect(el
, origin
) {
301 var offset
= el
.offset();
302 var scrollbarWidths
= getScrollbarWidths(el
);
303 var left
= offset
.left
+ getCssFloat(el
, 'border-left-width') + scrollbarWidths
.left
- (origin
? origin
.left
: 0);
304 var top
= offset
.top
+ getCssFloat(el
, 'border-top-width') + scrollbarWidths
.top
- (origin
? origin
.top
: 0);
308 right
: left
+ el
[0].clientWidth
, // clientWidth includes padding but NOT scrollbars
310 bottom
: top
+ el
[0].clientHeight
// clientHeight includes padding but NOT scrollbars
315 // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
316 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
317 // Origin is optional.
318 function getContentRect(el
, origin
) {
319 var offset
= el
.offset(); // just outside of border, margin not included
320 var left
= offset
.left
+ getCssFloat(el
, 'border-left-width') + getCssFloat(el
, 'padding-left') -
321 (origin
? origin
.left
: 0);
322 var top
= offset
.top
+ getCssFloat(el
, 'border-top-width') + getCssFloat(el
, 'padding-top') -
323 (origin
? origin
.top
: 0);
327 right
: left
+ el
.width(),
329 bottom
: top
+ el
.height()
334 // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
335 // WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger).
336 // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
337 function getScrollbarWidths(el
) {
338 var leftRightWidth
= el
[0].offsetWidth
- el
[0].clientWidth
;
339 var bottomWidth
= el
[0].offsetHeight
- el
[0].clientHeight
;
342 leftRightWidth
= sanitizeScrollbarWidth(leftRightWidth
);
343 bottomWidth
= sanitizeScrollbarWidth(bottomWidth
);
345 widths
= { left
: 0, right
: 0, top
: 0, bottom
: bottomWidth
};
347 if (getIsLeftRtlScrollbars() && el
.css('direction') == 'rtl') { // is the scrollbar on the left side?
348 widths
.left
= leftRightWidth
;
351 widths
.right
= leftRightWidth
;
358 // The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to
359 // retina displays, rounding, and IE11. Massage them into a usable value.
360 function sanitizeScrollbarWidth(width
) {
361 width
= Math
.max(0, width
); // no negatives
362 width
= Math
.round(width
);
367 // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
369 var _isLeftRtlScrollbars
= null;
371 function getIsLeftRtlScrollbars() { // responsible for caching the computation
372 if (_isLeftRtlScrollbars
=== null) {
373 _isLeftRtlScrollbars
= computeIsLeftRtlScrollbars();
375 return _isLeftRtlScrollbars
;
378 function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
379 var el
= $('<div><div/></div>')
381 position
: 'absolute',
390 var innerEl
= el
.children();
391 var res
= innerEl
.offset().left
> el
.offset().left
; // is the inner div shifted to accommodate a left scrollbar?
397 // Retrieves a jQuery element's computed CSS value as a floating-point number.
398 // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
399 function getCssFloat(el
, prop
) {
400 return parseFloat(el
.css(prop
)) || 0;
404 /* Mouse / Touch Utilities
405 ----------------------------------------------------------------------------------------------------------------------*/
407 FC
.preventDefault
= preventDefault
;
410 // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
411 function isPrimaryMouseButton(ev
) {
412 return ev
.which
== 1 && !ev
.ctrlKey
;
416 function getEvX(ev
) {
417 var touches
= ev
.originalEvent
.touches
;
419 // on mobile FF, pageX for touch events is present, but incorrect,
420 // so, look at touch coordinates first.
421 if (touches
&& touches
.length
) {
422 return touches
[0].pageX
;
429 function getEvY(ev
) {
430 var touches
= ev
.originalEvent
.touches
;
432 // on mobile FF, pageX for touch events is present, but incorrect,
433 // so, look at touch coordinates first.
434 if (touches
&& touches
.length
) {
435 return touches
[0].pageY
;
442 function getEvIsTouch(ev
) {
443 return /^touch/.test(ev
.type
);
447 function preventSelection(el
) {
448 el
.addClass('fc-unselectable')
449 .on('selectstart', preventDefault
);
453 function allowSelection(el
) {
454 el
.removeClass('fc-unselectable')
455 .off('selectstart', preventDefault
);
459 // Stops a mouse/touch event from doing it's native browser action
460 function preventDefault(ev
) {
465 /* General Geometry Utils
466 ----------------------------------------------------------------------------------------------------------------------*/
468 FC
.intersectRects
= intersectRects
;
470 // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
471 function intersectRects(rect1
, rect2
) {
473 left
: Math
.max(rect1
.left
, rect2
.left
),
474 right
: Math
.min(rect1
.right
, rect2
.right
),
475 top
: Math
.max(rect1
.top
, rect2
.top
),
476 bottom
: Math
.min(rect1
.bottom
, rect2
.bottom
)
479 if (res
.left
< res
.right
&& res
.top
< res
.bottom
) {
486 // Returns a new point that will have been moved to reside within the given rectangle
487 function constrainPoint(point
, rect
) {
489 left
: Math
.min(Math
.max(point
.left
, rect
.left
), rect
.right
),
490 top
: Math
.min(Math
.max(point
.top
, rect
.top
), rect
.bottom
)
495 // Returns a point that is the center of the given rectangle
496 function getRectCenter(rect
) {
498 left
: (rect
.left
+ rect
.right
) / 2,
499 top
: (rect
.top
+ rect
.bottom
) / 2
504 // Subtracts point2's coordinates from point1's coordinates, returning a delta
505 function diffPoints(point1
, point2
) {
507 left
: point1
.left
- point2
.left
,
508 top
: point1
.top
- point2
.top
513 /* Object Ordering by Field
514 ----------------------------------------------------------------------------------------------------------------------*/
516 FC
.parseFieldSpecs
= parseFieldSpecs
;
517 FC
.compareByFieldSpecs
= compareByFieldSpecs
;
518 FC
.compareByFieldSpec
= compareByFieldSpec
;
519 FC
.flexibleCompare
= flexibleCompare
;
522 function parseFieldSpecs(input
) {
527 if (typeof input
=== 'string') {
528 tokens
= input
.split(/\s*,\s*/);
530 else if (typeof input
=== 'function') {
533 else if ($.isArray(input
)) {
537 for (i
= 0; i
< tokens
.length
; i
++) {
540 if (typeof token
=== 'string') {
542 token
.charAt(0) == '-' ?
543 { field
: token
.substring(1), order
: -1 } :
544 { field
: token
, order
: 1 }
547 else if (typeof token
=== 'function') {
548 specs
.push({ func
: token
});
556 function compareByFieldSpecs(obj1
, obj2
, fieldSpecs
) {
560 for (i
= 0; i
< fieldSpecs
.length
; i
++) {
561 cmp
= compareByFieldSpec(obj1
, obj2
, fieldSpecs
[i
]);
571 function compareByFieldSpec(obj1
, obj2
, fieldSpec
) {
572 if (fieldSpec
.func
) {
573 return fieldSpec
.func(obj1
, obj2
);
575 return flexibleCompare(obj1
[fieldSpec
.field
], obj2
[fieldSpec
.field
]) *
576 (fieldSpec
.order
|| 1);
580 function flexibleCompare(a
, b
) {
590 if ($.type(a
) === 'string' || $.type(b
) === 'string') {
591 return String(a
).localeCompare(String(b
));
598 ----------------------------------------------------------------------------------------------------------------------*/
600 FC
.computeGreatestUnit
= computeGreatestUnit
;
601 FC
.divideRangeByDuration
= divideRangeByDuration
;
602 FC
.divideDurationByDuration
= divideDurationByDuration
;
603 FC
.multiplyDuration
= multiplyDuration
;
604 FC
.durationHasTime
= durationHasTime
;
606 var dayIDs
= [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
607 var unitsDesc
= [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending
610 // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
611 // Moments will have their timezones normalized.
612 function diffDayTime(a
, b
) {
613 return moment
.duration({
614 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days'),
615 ms
: a
.time() - b
.time() // time-of-day from day start. disregards timezone
620 // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
621 function diffDay(a
, b
) {
622 return moment
.duration({
623 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days')
628 // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
629 function diffByUnit(a
, b
, unit
) {
630 return moment
.duration(
631 Math
.round(a
.diff(b
, unit
, true)), // returnFloat=true
637 // Computes the unit name of the largest whole-unit period of time.
638 // For example, 48 hours will be "days" whereas 49 hours will be "hours".
639 // Accepts start/end, a range object, or an original duration object.
640 function computeGreatestUnit(start
, end
) {
644 for (i
= 0; i
< unitsDesc
.length
; i
++) {
646 val
= computeRangeAs(unit
, start
, end
);
648 if (val
>= 1 && isInt(val
)) {
653 return unit
; // will be "milliseconds" if nothing else matches
657 // like computeGreatestUnit, but has special abilities to interpret the source input for clues
658 function computeDurationGreatestUnit(duration
, durationInput
) {
659 var unit
= computeGreatestUnit(duration
);
661 // prevent days:7 from being interpreted as a week
662 if (unit
=== 'week' && typeof durationInput
=== 'object' && durationInput
.days
) {
670 // Computes the number of units (like "hours") in the given range.
671 // Range can be a {start,end} object, separate start/end args, or a Duration.
672 // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
673 // of month-diffing logic (which tends to vary from version to version).
674 function computeRangeAs(unit
, start
, end
) {
676 if (end
!= null) { // given start, end
677 return end
.diff(start
, unit
, true);
679 else if (moment
.isDuration(start
)) { // given duration
680 return start
.as(unit
);
682 else { // given { start, end } range object
683 return start
.end
.diff(start
.start
, unit
, true);
688 // Intelligently divides a range (specified by a start/end params) by a duration
689 function divideRangeByDuration(start
, end
, dur
) {
692 if (durationHasTime(dur
)) {
693 return (end
- start
) / dur
;
695 months
= dur
.asMonths();
696 if (Math
.abs(months
) >= 1 && isInt(months
)) {
697 return end
.diff(start
, 'months', true) / months
;
699 return end
.diff(start
, 'days', true) / dur
.asDays();
703 // Intelligently divides one duration by another
704 function divideDurationByDuration(dur1
, dur2
) {
705 var months1
, months2
;
707 if (durationHasTime(dur1
) || durationHasTime(dur2
)) {
710 months1
= dur1
.asMonths();
711 months2
= dur2
.asMonths();
713 Math
.abs(months1
) >= 1 && isInt(months1
) &&
714 Math
.abs(months2
) >= 1 && isInt(months2
)
716 return months1
/ months2
;
718 return dur1
.asDays() / dur2
.asDays();
722 // Intelligently multiplies a duration by a number
723 function multiplyDuration(dur
, n
) {
726 if (durationHasTime(dur
)) {
727 return moment
.duration(dur
* n
);
729 months
= dur
.asMonths();
730 if (Math
.abs(months
) >= 1 && isInt(months
)) {
731 return moment
.duration({ months
: months
* n
});
733 return moment
.duration({ days
: dur
.asDays() * n
});
737 // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
738 function durationHasTime(dur
) {
739 return Boolean(dur
.hours() || dur
.minutes() || dur
.seconds() || dur
.milliseconds());
743 function isNativeDate(input
) {
744 return Object
.prototype.toString
.call(input
) === '[object Date]' || input
instanceof Date
;
748 // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
749 function isTimeString(str
) {
750 return typeof str
=== 'string' &&
751 /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str
);
756 ----------------------------------------------------------------------------------------------------------------------*/
758 FC
.log = function() {
759 var console
= window
.console
;
761 if (console
&& console
.log
) {
762 return console
.log
.apply(console
, arguments
);
766 FC
.warn = function() {
767 var console
= window
.console
;
769 if (console
&& console
.warn
) {
770 return console
.warn
.apply(console
, arguments
);
773 return FC
.log
.apply(FC
, arguments
);
779 ----------------------------------------------------------------------------------------------------------------------*/
781 var hasOwnPropMethod
= {}.hasOwnProperty
;
784 // Merges an array of objects into a single object.
785 // The second argument allows for an array of property names who's object values will be merged together.
786 function mergeProps(propObjs
, complexProps
) {
794 for (i
= 0; i
< complexProps
.length
; i
++) {
795 name
= complexProps
[i
];
798 // collect the trailing object values, stopping when a non-object is discovered
799 for (j
= propObjs
.length
- 1; j
>= 0; j
--) {
800 val
= propObjs
[j
][name
];
802 if (typeof val
=== 'object') {
803 complexObjs
.unshift(val
);
805 else if (val
!== undefined) {
806 dest
[name
] = val
; // if there were no objects, this value will be used
811 // if the trailing values were objects, use the merged value
812 if (complexObjs
.length
) {
813 dest
[name
] = mergeProps(complexObjs
);
818 // copy values into the destination, going from last to first
819 for (i
= propObjs
.length
- 1; i
>= 0; i
--) {
822 for (name
in props
) {
823 if (!(name
in dest
)) { // if already assigned by previous props or complex props, don't reassign
824 dest
[name
] = props
[name
];
833 function copyOwnProps(src
, dest
) {
834 for (var name
in src
) {
835 if (hasOwnProp(src
, name
)) {
836 dest
[name
] = src
[name
];
842 function hasOwnProp(obj
, name
) {
843 return hasOwnPropMethod
.call(obj
, name
);
847 function applyAll(functions
, thisObj
, args
) {
848 if ($.isFunction(functions
)) {
849 functions
= [ functions
];
854 for (i
=0; i
<functions
.length
; i
++) {
855 ret
= functions
[i
].apply(thisObj
, args
) || ret
;
862 function removeMatching(array
, testFunc
) {
866 while (i
< array
.length
) {
867 if (testFunc(array
[i
])) { // truthy value means *remove*
880 function removeExact(array
, exactVal
) {
884 while (i
< array
.length
) {
885 if (array
[i
] === exactVal
) {
896 FC
.removeExact
= removeExact
;
900 function firstDefined() {
901 for (var i
=0; i
<arguments
.length
; i
++) {
902 if (arguments
[i
] !== undefined) {
909 function htmlEscape(s
) {
910 return (s
+ '').replace(/&/g
, '&')
911 .replace(/</g
, '<')
912 .replace(/>/g
, '>')
913 .replace(/'/g, ''')
914 .replace(/"/g, '"
;')
915 .replace(/\n/g, '<br
/>');
919 function stripHtmlEntities(text) {
920 return text.replace(/&.*?;/g, '');
924 // Given a hash of CSS properties, returns a string of CSS.
925 // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
926 function cssToStr(cssProps) {
929 $.each(cssProps, function(name, val) {
931 statements.push(name + ':' + val);
935 return statements.join(';');
939 // Given an object hash of HTML attribute names to values,
940 // generates a string that can be injected between < > in HTML
941 function attrsToStr(attrs) {
944 $.each(attrs, function(name, val) {
946 parts.push(name + '="' + htmlEscape(val) + '"');
950 return parts.join(' ');
954 function capitaliseFirstLetter(str) {
955 return str.charAt(0).toUpperCase() + str.slice(1);
959 function compareNumbers(a, b) { // for .sort()
969 // Returns a method bound to the given object context.
970 // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
971 // different contexts as identical when binding/unbinding events.
972 function proxy(obj, methodName) {
973 var method = obj[methodName];
976 return method.apply(obj, arguments);
981 // Returns a function, that, as long as it continues to be invoked, will not
982 // be triggered. The function will be called after it stops being called for
983 // N milliseconds. If `immediate` is passed, trigger the function on the
984 // leading edge, instead of the trailing.
985 // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
986 function debounce(func, wait, immediate) {
987 var timeout, args, context, timestamp, result;
989 var later = function() {
990 var last = +new Date() - timestamp;
992 timeout = setTimeout(later, wait - last);
997 result = func.apply(context, args);
998 context = args = null;
1006 timestamp = +new Date();
1007 var callNow = immediate && !timeout;
1009 timeout = setTimeout(later, wait);
1012 result = func.apply(context, args);
1013 context = args = null;
1022 GENERAL NOTE on moments throughout the *entire rest* of the codebase:
1023 All moments are assumed to be ambiguously-zoned unless otherwise noted,
1024 with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
1025 Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
1028 var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
1029 var ambigTimeOrZoneRegex =
1030 /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
1031 var newMomentProto = moment.fn; // where we will attach our new methods
1032 var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
1034 // tell momentjs to transfer these properties upon clone
1035 var momentProperties = moment.momentProperties;
1036 momentProperties.push('_fullCalendar
');
1037 momentProperties.push('_ambigTime
');
1038 momentProperties.push('_ambigZone
');
1042 // -------------------------------------------------------------------------------------------------
1044 // Creates a new moment, similar to the vanilla moment(...) constructor, but with
1045 // extra features (ambiguous time, enhanced formatting). When given an existing moment,
1046 // it will function as a clone (and retain the zone of the moment). Anything else will
1047 // result in a moment in the local zone.
1048 FC.moment = function() {
1049 return makeMoment(arguments);
1052 // Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
1053 FC.moment.utc = function() {
1054 var mom = makeMoment(arguments, true);
1056 // Force it into UTC because makeMoment doesn't guarantee it
1057 // (if given a pre-existing moment for example)
1058 if (mom
.hasTime()) { // don't give ambiguously-timed moments a UTC zone
1065 // Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
1066 // ISO8601 strings with no timezone offset will become ambiguously zoned.
1067 FC
.moment
.parseZone = function() {
1068 return makeMoment(arguments
, true, true);
1071 // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
1072 // native Date, or called with no arguments (the current time), the resulting moment will be local.
1073 // Anything else needs to be "parsed" (a string or an array), and will be affected by:
1074 // parseAsUTC - if there is no zone information, should we parse the input in UTC?
1075 // parseZone - if there is zone information, should we force the zone of the moment?
1076 function makeMoment(args
, parseAsUTC
, parseZone
) {
1077 var input
= args
[0];
1078 var isSingleString
= args
.length
== 1 && typeof input
=== 'string';
1084 if (moment
.isMoment(input
) || isNativeDate(input
) || input
=== undefined) {
1085 mom
= moment
.apply(null, args
);
1087 else { // "parsing" is required
1088 isAmbigTime
= false;
1089 isAmbigZone
= false;
1091 if (isSingleString
) {
1092 if (ambigDateOfMonthRegex
.test(input
)) {
1093 // accept strings like '2014-05', but convert to the first of the month
1095 args
= [ input
]; // for when we pass it on to moment's constructor
1099 else if ((ambigMatch
= ambigTimeOrZoneRegex
.exec(input
))) {
1100 isAmbigTime
= !ambigMatch
[5]; // no time part?
1104 else if ($.isArray(input
)) {
1105 // arrays have no timezone information, so assume ambiguous zone
1108 // otherwise, probably a string with a format
1110 if (parseAsUTC
|| isAmbigTime
) {
1111 mom
= moment
.utc
.apply(moment
, args
);
1114 mom
= moment
.apply(null, args
);
1118 mom
._ambigTime
= true;
1119 mom
._ambigZone
= true; // ambiguous time always means ambiguous zone
1121 else if (parseZone
) { // let's record the inputted zone somehow
1123 mom
._ambigZone
= true;
1125 else if (isSingleString
) {
1126 mom
.utcOffset(input
); // if not a valid zone, will assign UTC
1131 mom
._fullCalendar
= true; // flag for extended functionality
1138 // -------------------------------------------------------------------------------------------------
1141 // Returns the week number, considering the locale's custom week number calcuation
1142 // `weeks` is an alias for `week`
1143 newMomentProto
.week
= newMomentProto
.weeks = function(input
) {
1144 var weekCalc
= this._locale
._fullCalendar_weekCalc
;
1146 if (input
== null && typeof weekCalc
=== 'function') { // custom function only works for getter
1147 return weekCalc(this);
1149 else if (weekCalc
=== 'ISO') {
1150 return oldMomentProto
.isoWeek
.apply(this, arguments
); // ISO getter/setter
1153 return oldMomentProto
.week
.apply(this, arguments
); // local getter/setter
1158 // -------------------------------------------------------------------------------------------------
1161 // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
1162 // If the moment has an ambiguous time, a duration of 00:00 will be returned.
1165 // You can supply a Duration, a Moment, or a Duration-like argument.
1166 // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
1167 newMomentProto
.time = function(time
) {
1169 // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
1170 // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
1171 if (!this._fullCalendar
) {
1172 return oldMomentProto
.time
.apply(this, arguments
);
1175 if (time
== null) { // getter
1176 return moment
.duration({
1177 hours
: this.hours(),
1178 minutes
: this.minutes(),
1179 seconds
: this.seconds(),
1180 milliseconds
: this.milliseconds()
1185 this._ambigTime
= false; // mark that the moment now has a time
1187 if (!moment
.isDuration(time
) && !moment
.isMoment(time
)) {
1188 time
= moment
.duration(time
);
1191 // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
1192 // Only for Duration times, not Moment times.
1194 if (moment
.isDuration(time
)) {
1195 dayHours
= Math
.floor(time
.asDays()) * 24;
1198 // We need to set the individual fields.
1199 // Can't use startOf('day') then add duration. In case of DST at start of day.
1200 return this.hours(dayHours
+ time
.hours())
1201 .minutes(time
.minutes())
1202 .seconds(time
.seconds())
1203 .milliseconds(time
.milliseconds());
1207 // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
1208 // but preserving its YMD. A moment with a stripped time will display no time
1209 // nor timezone offset when .format() is called.
1210 newMomentProto
.stripTime = function() {
1212 if (!this._ambigTime
) {
1214 this.utc(true); // keepLocalTime=true (for keeping *date* value)
1224 // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1225 // which clears all ambig flags.
1226 this._ambigTime
= true;
1227 this._ambigZone
= true; // if ambiguous time, also ambiguous timezone offset
1230 return this; // for chaining
1233 // Returns if the moment has a non-ambiguous time (boolean)
1234 newMomentProto
.hasTime = function() {
1235 return !this._ambigTime
;
1240 // -------------------------------------------------------------------------------------------------
1242 // Converts the moment to UTC, stripping out its timezone offset, but preserving its
1243 // YMD and time-of-day. A moment with a stripped timezone offset will display no
1244 // timezone offset when .format() is called.
1245 newMomentProto
.stripZone = function() {
1248 if (!this._ambigZone
) {
1250 wasAmbigTime
= this._ambigTime
;
1252 this.utc(true); // keepLocalTime=true (for keeping date and time values)
1254 // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
1255 this._ambigTime
= wasAmbigTime
|| false;
1257 // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1258 // which clears the ambig flags.
1259 this._ambigZone
= true;
1262 return this; // for chaining
1265 // Returns of the moment has a non-ambiguous timezone offset (boolean)
1266 newMomentProto
.hasZone = function() {
1267 return !this._ambigZone
;
1271 // implicitly marks a zone
1272 newMomentProto
.local = function(keepLocalTime
) {
1274 // for when converting from ambiguously-zoned to local,
1275 // keep the time values when converting from UTC -> local
1276 oldMomentProto
.local
.call(this, this._ambigZone
|| keepLocalTime
);
1278 // ensure non-ambiguous
1279 // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
1280 this._ambigTime
= false;
1281 this._ambigZone
= false;
1283 return this; // for chaining
1287 // implicitly marks a zone
1288 newMomentProto
.utc = function(keepLocalTime
) {
1290 oldMomentProto
.utc
.call(this, keepLocalTime
);
1292 // ensure non-ambiguous
1293 // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
1294 this._ambigTime
= false;
1295 this._ambigZone
= false;
1301 // implicitly marks a zone (will probably get called upon .utc() and .local())
1302 newMomentProto
.utcOffset = function(tzo
) {
1304 if (tzo
!= null) { // setter
1305 // these assignments needs to happen before the original zone method is called.
1306 // I forget why, something to do with a browser crash.
1307 this._ambigTime
= false;
1308 this._ambigZone
= false;
1311 return oldMomentProto
.utcOffset
.apply(this, arguments
);
1316 // -------------------------------------------------------------------------------------------------
1318 newMomentProto
.format = function() {
1320 if (this._fullCalendar
&& arguments
[0]) { // an enhanced moment? and a format string provided?
1321 return formatDate(this, arguments
[0]); // our extended formatting
1323 if (this._ambigTime
) {
1324 return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
1326 if (this._ambigZone
) {
1327 return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
1329 if (this._fullCalendar
) { // enhanced non-ambig moment?
1330 // moment.format() doesn't ensure english, but we want to.
1331 return oldMomentFormat(englishMoment(this));
1334 return oldMomentProto
.format
.apply(this, arguments
);
1337 newMomentProto
.toISOString = function() {
1339 if (this._ambigTime
) {
1340 return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
1342 if (this._ambigZone
) {
1343 return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
1345 if (this._fullCalendar
) { // enhanced non-ambig moment?
1346 // depending on browser, moment might not output english. ensure english.
1347 // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
1348 return oldMomentProto
.toISOString
.apply(englishMoment(this), arguments
);
1351 return oldMomentProto
.toISOString
.apply(this, arguments
);
1354 function englishMoment(mom
) {
1355 if (mom
.locale() !== 'en') {
1356 return mom
.clone().locale('en');
1365 FC
.formatDate
= formatDate
;
1366 FC
.formatRange
= formatRange
;
1367 FC
.oldMomentFormat
= oldMomentFormat
;
1368 FC
.queryMostGranularFormatUnit
= queryMostGranularFormatUnit
;
1372 // ---------------------------------------------------------------------------------------------------------------------
1375 Inserted between chunks in the fake ("intermediate") formatting string.
1376 Important that it passes as whitespace (\s) because moment often identifies non-standalone months
1377 via a regexp with an \s.
1379 var PART_SEPARATOR
= '\u000b'; // vertical tab
1382 Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text,
1383 but rather, a "special" token that has custom rendering (see specialTokens map).
1385 var SPECIAL_TOKEN_MARKER
= '\u001f'; // information separator 1
1388 Inserted at the beginning and end of a span of text that must have non-zero numeric characters.
1389 Handling of these markers is done in a post-processing step at the very end of text rendering.
1391 var MAYBE_MARKER
= '\u001e'; // information separator 2
1392 var MAYBE_REGEXP
= new RegExp(MAYBE_MARKER
+ '([^' + MAYBE_MARKER
+ ']*)' + MAYBE_MARKER
, 'g'); // must be global
1395 Addition formatting tokens we want recognized
1397 var specialTokens
= {
1398 t: function(date
) { // "a" or "p"
1399 return oldMomentFormat(date
, 'a').charAt(0);
1401 T: function(date
) { // "A" or "P"
1402 return oldMomentFormat(date
, 'A').charAt(0);
1407 The first characters of formatting tokens for units that are 1 day or larger.
1408 `value` is for ranking relative size (lower means bigger).
1409 `unit` is a normalized unit, used for comparing moments.
1411 var largeTokenMap
= {
1412 Y
: { value
: 1, unit
: 'year' },
1413 M
: { value
: 2, unit
: 'month' },
1414 W
: { value
: 3, unit
: 'week' }, // ISO week
1415 w
: { value
: 3, unit
: 'week' }, // local week
1416 D
: { value
: 4, unit
: 'day' }, // day of month
1417 d
: { value
: 4, unit
: 'day' } // day of week
1421 // Single Date Formatting
1422 // ---------------------------------------------------------------------------------------------------------------------
1425 Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
1427 function formatDate(date
, formatStr
) {
1428 return renderFakeFormatString(
1429 getParsedFormatString(formatStr
).fakeFormatString
,
1435 Call this if you want Moment's original format method to be used
1437 function oldMomentFormat(mom
, formatStr
) {
1438 return oldMomentProto
.format
.call(mom
, formatStr
); // oldMomentProto defined in moment-ext.js
1442 // Date Range Formatting
1443 // -------------------------------------------------------------------------------------------------
1444 // TODO: make it work with timezone offset
1447 Using a formatting string meant for a single date, generate a range string, like
1448 "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1449 If the dates are the same as far as the format string is concerned, just return a single
1450 rendering of one date, without any separator.
1452 function formatRange(date1
, date2
, formatStr
, separator
, isRTL
) {
1455 date1
= FC
.moment
.parseZone(date1
);
1456 date2
= FC
.moment
.parseZone(date2
);
1458 localeData
= date1
.localeData();
1460 // Expand localized format strings, like "LL" -> "MMMM D YYYY".
1461 // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1462 // or non-zero areas in Moment's localized format strings.
1463 formatStr
= localeData
.longDateFormat(formatStr
) || formatStr
;
1465 return renderParsedFormat(
1466 getParsedFormatString(formatStr
),
1475 Renders a range with an already-parsed format string.
1477 function renderParsedFormat(parsedFormat
, date1
, date2
, separator
, isRTL
) {
1478 var sameUnits
= parsedFormat
.sameUnits
;
1479 var unzonedDate1
= date1
.clone().stripZone(); // for same-unit comparisons
1480 var unzonedDate2
= date2
.clone().stripZone(); // "
1482 var renderedParts1
= renderFakeFormatStringParts(parsedFormat
.fakeFormatString
, date1
);
1483 var renderedParts2
= renderFakeFormatStringParts(parsedFormat
.fakeFormatString
, date2
);
1490 var middleStr1
= '';
1491 var middleStr2
= '';
1494 // Start at the leftmost side of the formatting string and continue until you hit a token
1495 // that is not the same between dates.
1498 leftI
< sameUnits
.length
&& (!sameUnits
[leftI
] || unzonedDate1
.isSame(unzonedDate2
, sameUnits
[leftI
]));
1501 leftStr
+= renderedParts1
[leftI
];
1504 // Similarly, start at the rightmost side of the formatting string and move left
1506 rightI
= sameUnits
.length
- 1;
1507 rightI
> leftI
&& (!sameUnits
[rightI
] || unzonedDate1
.isSame(unzonedDate2
, sameUnits
[rightI
]));
1510 // If current chunk is on the boundary of unique date-content, and is a special-case
1511 // date-formatting postfix character, then don't consume it. Consider it unique date-content.
1512 // TODO: make configurable
1513 if (rightI
- 1 === leftI
&& renderedParts1
[rightI
] === '.') {
1517 rightStr
= renderedParts1
[rightI
] + rightStr
;
1520 // The area in the middle is different for both of the dates.
1521 // Collect them distinctly so we can jam them together later.
1522 for (middleI
= leftI
; middleI
<= rightI
; middleI
++) {
1523 middleStr1
+= renderedParts1
[middleI
];
1524 middleStr2
+= renderedParts2
[middleI
];
1527 if (middleStr1
|| middleStr2
) {
1529 middleStr
= middleStr2
+ separator
+ middleStr1
;
1532 middleStr
= middleStr1
+ separator
+ middleStr2
;
1536 return processMaybeMarkers(
1537 leftStr
+ middleStr
+ rightStr
1542 // Format String Parsing
1543 // ---------------------------------------------------------------------------------------------------------------------
1545 var parsedFormatStrCache
= {};
1548 Returns a parsed format string, leveraging a cache.
1550 function getParsedFormatString(formatStr
) {
1551 return parsedFormatStrCache
[formatStr
] ||
1552 (parsedFormatStrCache
[formatStr
] = parseFormatString(formatStr
));
1556 Parses a format string into the following:
1557 - fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed.
1558 - sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"),
1559 that indicates how similar a range's start & end must be in order to share the same formatted text.
1560 If not a token, then the value is null.
1561 Always a flat array (not nested liked "chunks").
1563 function parseFormatString(formatStr
) {
1564 var chunks
= chunkFormatString(formatStr
);
1567 fakeFormatString
: buildFakeFormatString(chunks
),
1568 sameUnits
: buildSameUnits(chunks
)
1573 Break the formatting string into an array of chunks.
1574 A 'maybe' chunk will have nested chunks.
1576 function chunkFormatString(formatStr
) {
1580 // TODO: more descrimination
1581 // \4 is a backreference to the first character of a multi-character set.
1582 var chunker
= /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;
1584 while ((match
= chunker
.exec(formatStr
))) {
1585 if (match
[1]) { // a literal string inside [ ... ]
1586 chunks
.push
.apply(chunks
, // append
1587 splitStringLiteral(match
[1])
1590 else if (match
[2]) { // non-zero formatting inside ( ... )
1591 chunks
.push({ maybe
: chunkFormatString(match
[2]) });
1593 else if (match
[3]) { // a formatting token
1594 chunks
.push({ token
: match
[3] });
1596 else if (match
[5]) { // an unenclosed literal string
1597 chunks
.push
.apply(chunks
, // append
1598 splitStringLiteral(match
[5])
1607 Potentially splits a literal-text string into multiple parts. For special cases.
1609 function splitStringLiteral(s
) {
1611 return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date
1619 Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control
1620 characters that will eventually be given to moment for formatting, and then post-processed.
1622 function buildFakeFormatString(chunks
) {
1626 for (i
= 0; i
< chunks
.length
; i
++) {
1629 if (typeof chunk
=== 'string') {
1630 parts
.push('[' + chunk
+ ']');
1632 else if (chunk
.token
) {
1633 if (chunk
.token
in specialTokens
) {
1635 SPECIAL_TOKEN_MARKER
+ // useful during post-processing
1636 '[' + chunk
.token
+ ']' // preserve as literal text
1640 parts
.push(chunk
.token
); // unprotected text implies a format string
1643 else if (chunk
.maybe
) {
1645 MAYBE_MARKER
+ // useful during post-processing
1646 buildFakeFormatString(chunk
.maybe
) +
1652 return parts
.join(PART_SEPARATOR
);
1656 Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate
1657 in which regard two dates must be similar in order to share range formatting text.
1658 The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat.
1660 function buildSameUnits(chunks
) {
1665 for (i
= 0; i
< chunks
.length
; i
++) {
1669 tokenInfo
= largeTokenMap
[chunk
.token
.charAt(0)];
1670 units
.push(tokenInfo
? tokenInfo
.unit
: 'second'); // default to a very strict same-second
1672 else if (chunk
.maybe
) {
1673 units
.push
.apply(units
, // append
1674 buildSameUnits(chunk
.maybe
)
1686 // Rendering to text
1687 // ---------------------------------------------------------------------------------------------------------------------
1690 Formats a date with a fake format string, post-processes the control characters, then returns.
1692 function renderFakeFormatString(fakeFormatString
, date
) {
1693 return processMaybeMarkers(
1694 renderFakeFormatStringParts(fakeFormatString
, date
).join('')
1699 Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers.
1701 function renderFakeFormatStringParts(fakeFormatString
, date
) {
1703 var fakeRender
= oldMomentFormat(date
, fakeFormatString
);
1704 var fakeParts
= fakeRender
.split(PART_SEPARATOR
);
1707 for (i
= 0; i
< fakeParts
.length
; i
++) {
1708 fakePart
= fakeParts
[i
];
1710 if (fakePart
.charAt(0) === SPECIAL_TOKEN_MARKER
) {
1712 // the literal string IS the token's name.
1713 // call special token's registered function.
1714 specialTokens
[fakePart
.substring(1)](date
)
1718 parts
.push(fakePart
);
1726 Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string.
1728 function processMaybeMarkers(s
) {
1729 return s
.replace(MAYBE_REGEXP
, function(m0
, m1
) { // regex assumed to have 'g' flag
1730 if (m1
.match(/[1-9]/)) { // any non-zero numeric characters?
1741 // -------------------------------------------------------------------------------------------------
1744 Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
1746 function queryMostGranularFormatUnit(formatStr
) {
1747 var chunks
= chunkFormatString(formatStr
);
1752 for (i
= 0; i
< chunks
.length
; i
++) {
1756 candidate
= largeTokenMap
[chunk
.token
.charAt(0)];
1758 if (!best
|| candidate
.value
> best
.value
) {
1774 // quick local references
1775 var formatDate
= FC
.formatDate
;
1776 var formatRange
= FC
.formatRange
;
1777 var oldMomentFormat
= FC
.oldMomentFormat
;
1781 FC
.Class
= Class
; // export
1783 // Class that all other classes will inherit from
1784 function Class() { }
1787 // Called on a class to create a subclass.
1788 // Last argument contains instance methods. Any argument before the last are considered mixins.
1789 Class
.extend = function() {
1793 for (i
= 0; i
< arguments
.length
; i
++) {
1794 copyOwnProps(arguments
[i
], members
);
1797 return extendClass(this, members
);
1801 // Adds new member variables/methods to the class's prototype.
1802 // Can be called with another class, or a plain object hash containing new members.
1803 Class
.mixin = function(members
) {
1804 copyOwnProps(members
, this.prototype);
1808 function extendClass(superClass
, members
) {
1811 // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
1812 if (hasOwnProp(members
, 'constructor')) {
1813 subClass
= members
.constructor;
1815 if (typeof subClass
!== 'function') {
1816 subClass
= members
.constructor = function() {
1817 superClass
.apply(this, arguments
);
1821 // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
1822 subClass
.prototype = Object
.create(superClass
.prototype);
1824 // copy each member variable/method onto the the subclass's prototype
1825 copyOwnProps(members
, subClass
.prototype);
1827 // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1828 copyOwnProps(superClass
, subClass
);
1835 var EmitterMixin
= FC
.EmitterMixin
= {
1837 // jQuery-ification via $(this) allows a non-DOM object to have
1838 // the same event handling capabilities (including namespaces).
1841 on: function(types
, handler
) {
1842 $(this).on(types
, this._prepareIntercept(handler
));
1843 return this; // for chaining
1847 one: function(types
, handler
) {
1848 $(this).one(types
, this._prepareIntercept(handler
));
1849 return this; // for chaining
1853 _prepareIntercept: function(handler
) {
1854 // handlers are always called with an "event" object as their first param.
1855 // sneak the `this` context and arguments into the extra parameter object
1856 // and forward them on to the original handler.
1857 var intercept = function(ev
, extra
) {
1858 return handler
.apply(
1859 extra
.context
|| this,
1864 // mimick jQuery's internal "proxy" system (risky, I know)
1865 // causing all functions with the same .guid to appear to be the same.
1866 // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
1867 // this is needed for calling .off with the original non-intercept handler.
1868 if (!handler
.guid
) {
1869 handler
.guid
= $.guid
++;
1871 intercept
.guid
= handler
.guid
;
1877 off: function(types
, handler
) {
1878 $(this).off(types
, handler
);
1880 return this; // for chaining
1884 trigger: function(types
) {
1885 var args
= Array
.prototype.slice
.call(arguments
, 1); // arguments after the first
1887 // pass in "extra" info to the intercept
1888 $(this).triggerHandler(types
, { args
: args
});
1890 return this; // for chaining
1894 triggerWith: function(types
, context
, args
) {
1896 // `triggerHandler` is less reliant on the DOM compared to `trigger`.
1897 // pass in "extra" info to the intercept.
1898 $(this).triggerHandler(types
, { context
: context
, args
: args
});
1900 return this; // for chaining
1904 hasHandlers: function(type
) {
1905 var hash
= $._data(this, 'events'); // http://blog.jquery.com/2012/08/09/jquery-1-8-released/
1907 return hash
&& hash
[type
] && hash
[type
].length
> 0;
1915 Utility methods for easily listening to events on another object,
1916 and more importantly, easily unlistening from them.
1918 var ListenerMixin
= FC
.ListenerMixin
= (function() {
1920 var ListenerMixin
= {
1925 Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
1926 The `callback` will be called with the `this` context of the object that .listenTo is being called on.
1928 .listenTo(other, eventName, callback)
1931 eventName1: callback1,
1932 eventName2: callback2
1935 listenTo: function(other
, arg
, callback
) {
1936 if (typeof arg
=== 'object') { // given dictionary of callbacks
1937 for (var eventName
in arg
) {
1938 if (arg
.hasOwnProperty(eventName
)) {
1939 this.listenTo(other
, eventName
, arg
[eventName
]);
1943 else if (typeof arg
=== 'string') {
1945 arg
+ '.' + this.getListenerNamespace(), // use event namespacing to identify this object
1946 $.proxy(callback
, this) // always use `this` context
1947 // the usually-undesired jQuery guid behavior doesn't matter,
1948 // because we always unbind via namespace
1954 Causes the current object to stop listening to events on the `other` object.
1955 `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
1957 stopListeningTo: function(other
, eventName
) {
1958 other
.off((eventName
|| '') + '.' + this.getListenerNamespace());
1962 Returns a string, unique to this object, to be used for event namespacing
1964 getListenerNamespace: function() {
1965 if (this.listenerId
== null) {
1966 this.listenerId
= guid
++;
1968 return '_listener' + this.listenerId
;
1972 return ListenerMixin
;
1976 var ParsableModelMixin
= {
1978 standardPropMap
: {}, // will be cloned by allowRawProps
1982 Returns true/false for success
1984 applyRawProps: function(rawProps
) {
1985 var standardPropMap
= this.standardPropMap
;
1986 var manualProps
= {};
1987 var otherProps
= {};
1990 for (propName
in rawProps
) {
1991 if (standardPropMap
[propName
] === true) { // copy automatically
1992 this[propName
] = rawProps
[propName
];
1994 else if (standardPropMap
[propName
] === false) {
1995 manualProps
[propName
] = rawProps
[propName
];
1998 otherProps
[propName
] = rawProps
[propName
];
2002 this.applyOtherRawProps(otherProps
);
2004 return this.applyManualRawProps(manualProps
);
2009 If subclasses override, they must call this supermethod and return the boolean response.
2011 applyManualRawProps: function(rawProps
) {
2016 applyOtherRawProps: function(rawProps
) {
2017 // subclasses can implement
2024 TODO: devise a better system
2026 var ParsableModelMixin_allowRawProps = function(propDefs
) {
2027 var proto
= this.prototype;
2029 proto
.standardPropMap
= Object
.create(proto
.standardPropMap
);
2031 copyOwnProps(propDefs
, proto
.standardPropMap
);
2036 TODO: devise a better system
2038 var ParsableModelMixin_copyVerbatimStandardProps = function(src
, dest
) {
2039 var map
= this.prototype.standardPropMap
;
2042 for (propName
in map
) {
2044 src
[propName
] != null && // in the src object?
2045 map
[propName
] === true // false means "copy verbatim"
2047 dest
[propName
] = src
[propName
];
2054 var Model
= Class
.extend(EmitterMixin
, ListenerMixin
, {
2058 _globalWatchArgs
: null,
2060 constructor: function() {
2061 this._watchers
= {};
2063 this.applyGlobalWatchers();
2066 applyGlobalWatchers: function() {
2067 var argSets
= this._globalWatchArgs
|| [];
2070 for (i
= 0; i
< argSets
.length
; i
++) {
2071 this.watch
.apply(this, argSets
[i
]);
2075 has: function(name
) {
2076 return name
in this._props
;
2079 get: function(name
) {
2080 if (name
=== undefined) {
2084 return this._props
[name
];
2087 set: function(name
, val
) {
2090 if (typeof name
=== 'string') {
2092 newProps
[name
] = val
=== undefined ? null : val
;
2098 this.setProps(newProps
);
2101 reset: function(newProps
) {
2102 var oldProps
= this._props
;
2103 var changeset
= {}; // will have undefined's to signal unsets
2106 for (name
in oldProps
) {
2107 changeset
[name
] = undefined;
2110 for (name
in newProps
) {
2111 changeset
[name
] = newProps
[name
];
2114 this.setProps(changeset
);
2117 unset: function(name
) { // accepts a string or array of strings
2122 if (typeof name
=== 'string') {
2129 for (i
= 0; i
< names
.length
; i
++) {
2130 newProps
[names
[i
]] = undefined;
2133 this.setProps(newProps
);
2136 setProps: function(newProps
) {
2137 var changedProps
= {};
2141 for (name
in newProps
) {
2142 val
= newProps
[name
];
2144 // a change in value?
2145 // if an object, don't check equality, because might have been mutated internally.
2146 // TODO: eventually enforce immutability.
2148 typeof val
=== 'object' ||
2149 val
!== this._props
[name
]
2151 changedProps
[name
] = val
;
2158 this.trigger('before:batchChange', changedProps
);
2160 for (name
in changedProps
) {
2161 val
= changedProps
[name
];
2163 this.trigger('before:change', name
, val
);
2164 this.trigger('before:change:' + name
, val
);
2167 for (name
in changedProps
) {
2168 val
= changedProps
[name
];
2170 if (val
=== undefined) {
2171 delete this._props
[name
];
2174 this._props
[name
] = val
;
2177 this.trigger('change:' + name
, val
);
2178 this.trigger('change', name
, val
);
2181 this.trigger('batchChange', changedProps
);
2185 watch: function(name
, depList
, startFunc
, stopFunc
) {
2190 this._watchers
[name
] = this._watchDeps(depList
, function(deps
) {
2191 var res
= startFunc
.call(_this
, deps
);
2193 if (res
&& res
.then
) {
2194 _this
.unset(name
); // put in an unset state while resolving
2195 res
.then(function(val
) {
2196 _this
.set(name
, val
);
2200 _this
.set(name
, res
);
2206 stopFunc
.call(_this
);
2211 unwatch: function(name
) {
2212 var watcher
= this._watchers
[name
];
2215 delete this._watchers
[name
];
2220 _watchDeps: function(depList
, startFunc
, stopFunc
) {
2222 var queuedChangeCnt
= 0;
2223 var depCnt
= depList
.length
;
2225 var values
= {}; // what's passed as the `deps` arguments
2226 var bindTuples
= []; // array of [ eventName, handlerFunc ] arrays
2227 var isCallingStop
= false;
2229 function onBeforeDepChange(depName
, val
, isOptional
) {
2231 if (queuedChangeCnt
=== 1) { // first change to cause a "stop" ?
2232 if (satisfyCnt
=== depCnt
) { // all deps previously satisfied?
2233 isCallingStop
= true;
2235 isCallingStop
= false;
2240 function onDepChange(depName
, val
, isOptional
) {
2242 if (val
=== undefined) { // unsetting a value?
2244 // required dependency that was previously set?
2245 if (!isOptional
&& values
[depName
] !== undefined) {
2249 delete values
[depName
];
2251 else { // setting a value?
2253 // required dependency that was previously unset?
2254 if (!isOptional
&& values
[depName
] === undefined) {
2258 values
[depName
] = val
;
2262 if (!queuedChangeCnt
) { // last change to cause a "start"?
2264 // now finally satisfied or satisfied all along?
2265 if (satisfyCnt
=== depCnt
) {
2267 // if the stopFunc initiated another value change, ignore it.
2268 // it will be processed by another change event anyway.
2269 if (!isCallingStop
) {
2276 // intercept for .on() that remembers handlers
2277 function bind(eventName
, handler
) {
2278 _this
.on(eventName
, handler
);
2279 bindTuples
.push([ eventName
, handler
]);
2282 // listen to dependency changes
2283 depList
.forEach(function(depName
) {
2284 var isOptional
= false;
2286 if (depName
.charAt(0) === '?') { // TODO: more DRY
2287 depName
= depName
.substring(1);
2291 bind('before:change:' + depName
, function(val
) {
2292 onBeforeDepChange(depName
, val
, isOptional
);
2295 bind('change:' + depName
, function(val
) {
2296 onDepChange(depName
, val
, isOptional
);
2300 // process current dependency values
2301 depList
.forEach(function(depName
) {
2302 var isOptional
= false;
2304 if (depName
.charAt(0) === '?') { // TODO: more DRY
2305 depName
= depName
.substring(1);
2309 if (_this
.has(depName
)) {
2310 values
[depName
] = _this
.get(depName
);
2313 else if (isOptional
) {
2318 // initially satisfied
2319 if (satisfyCnt
=== depCnt
) {
2324 teardown: function() {
2325 // remove all handlers
2326 for (var i
= 0; i
< bindTuples
.length
; i
++) {
2327 _this
.off(bindTuples
[i
][0], bindTuples
[i
][1]);
2331 // was satisfied, so call stopFunc
2332 if (satisfyCnt
=== depCnt
) {
2337 if (satisfyCnt
=== depCnt
) {
2345 flash: function(name
) {
2346 var watcher
= this._watchers
[name
];
2356 Model
.watch = function(/* same arguments as this.watch() */) {
2357 var proto
= this.prototype;
2359 if (!proto
._globalWatchArgs
) {
2360 proto
._globalWatchArgs
= [];
2363 proto
._globalWatchArgs
.push(arguments
);
2374 construct: function(executor
) {
2375 var deferred
= $.Deferred();
2376 var promise
= deferred
.promise();
2378 if (typeof executor
=== 'function') {
2380 function(val
) { // resolve
2381 deferred
.resolve(val
);
2382 attachImmediatelyResolvingThen(promise
, val
);
2384 function() { // reject
2386 attachImmediatelyRejectingThen(promise
);
2394 resolve: function(val
) {
2395 var deferred
= $.Deferred().resolve(val
);
2396 var promise
= deferred
.promise();
2398 attachImmediatelyResolvingThen(promise
, val
);
2403 reject: function() {
2404 var deferred
= $.Deferred().reject();
2405 var promise
= deferred
.promise();
2407 attachImmediatelyRejectingThen(promise
);
2415 function attachImmediatelyResolvingThen(promise
, val
) {
2416 promise
.then = function(onResolve
) {
2417 if (typeof onResolve
=== 'function') {
2418 return Promise
.resolve(onResolve(val
));
2425 function attachImmediatelyRejectingThen(promise
) {
2426 promise
.then = function(onResolve
, onReject
) {
2427 if (typeof onReject
=== 'function') {
2435 FC
.Promise
= Promise
;
2439 var TaskQueue
= Class
.extend(EmitterMixin
, {
2446 constructor: function() {
2451 queue: function(/* taskFunc, taskFunc... */) {
2452 this.q
.push
.apply(this.q
, arguments
); // append
2458 this.isPaused
= true;
2462 resume: function() {
2463 this.isPaused
= false;
2468 tryStart: function() {
2469 if (!this.isRunning
&& this.canRunNext()) {
2470 this.isRunning
= true;
2471 this.trigger('start');
2477 canRunNext: function() {
2478 return !this.isPaused
&& this.q
.length
;
2482 runNext: function() { // does not check canRunNext
2483 this.runTask(this.q
.shift());
2487 runTask: function(task
) {
2488 this.runTaskFunc(task
);
2492 runTaskFunc: function(taskFunc
) {
2494 var res
= taskFunc();
2496 if (res
&& res
.then
) {
2504 if (_this
.canRunNext()) {
2508 _this
.isRunning
= false;
2509 _this
.trigger('stop');
2516 FC
.TaskQueue
= TaskQueue
;
2520 var RenderQueue
= TaskQueue
.extend({
2522 waitsByNamespace
: null,
2523 waitNamespace
: null,
2527 constructor: function(waitsByNamespace
) {
2528 TaskQueue
.call(this); // super-constructor
2530 this.waitsByNamespace
= waitsByNamespace
|| {};
2534 queue: function(taskFunc
, namespace, type
) {
2537 namespace: namespace,
2543 waitMs
= this.waitsByNamespace
[namespace];
2546 if (this.waitNamespace
) {
2547 if (namespace === this.waitNamespace
&& waitMs
!= null) {
2548 this.delayWait(waitMs
);
2556 if (this.compoundTask(task
)) { // appended to queue?
2558 if (!this.waitNamespace
&& waitMs
!= null) {
2559 this.startWait(namespace, waitMs
);
2568 startWait: function(namespace, waitMs
) {
2569 this.waitNamespace
= namespace;
2570 this.spawnWait(waitMs
);
2574 delayWait: function(waitMs
) {
2575 clearTimeout(this.waitId
);
2576 this.spawnWait(waitMs
);
2580 spawnWait: function(waitMs
) {
2583 this.waitId
= setTimeout(function() {
2584 _this
.waitNamespace
= null;
2590 clearWait: function() {
2591 if (this.waitNamespace
) {
2592 clearTimeout(this.waitId
);
2594 this.waitNamespace
= null;
2599 canRunNext: function() {
2600 if (!TaskQueue
.prototype.canRunNext
.apply(this, arguments
)) {
2604 // waiting for a certain namespace to stop receiving tasks?
2605 if (this.waitNamespace
) {
2607 // if there was a different namespace task in the meantime,
2608 // that forces all previously-waiting tasks to suddenly execute.
2609 // TODO: find a way to do this in constant time.
2610 for (var q
= this.q
, i
= 0; i
< q
.length
; i
++) {
2611 if (q
[i
].namespace !== this.waitNamespace
) {
2612 return true; // allow execution
2623 runTask: function(task
) {
2624 this.runTaskFunc(task
.func
);
2628 compoundTask: function(newTask
) {
2630 var shouldAppend
= true;
2633 if (newTask
.namespace) {
2635 if (newTask
.type
=== 'destroy' || newTask
.type
=== 'init') {
2637 // remove all add/remove ops with same namespace, regardless of order
2638 for (i
= q
.length
- 1; i
>= 0; i
--) {
2642 task
.namespace === newTask
.namespace &&
2643 (task
.type
=== 'add' || task
.type
=== 'remove')
2645 q
.splice(i
, 1); // remove task
2649 if (newTask
.type
=== 'destroy') {
2650 // eat away final init/destroy operation
2652 task
= q
[q
.length
- 1]; // last task
2654 if (task
.namespace === newTask
.namespace) {
2656 // the init and our destroy cancel each other out
2657 if (task
.type
=== 'init') {
2658 shouldAppend
= false;
2661 // prefer to use the destroy operation that's already present
2662 else if (task
.type
=== 'destroy') {
2663 shouldAppend
= false;
2668 else if (newTask
.type
=== 'init') {
2669 // eat away final init operation
2671 task
= q
[q
.length
- 1]; // last task
2674 task
.namespace === newTask
.namespace &&
2675 task
.type
=== 'init'
2677 // our init operation takes precedence
2689 return shouldAppend
;
2694 FC
.RenderQueue
= RenderQueue
;
2698 /* A rectangular panel that is absolutely positioned over other content
2699 ------------------------------------------------------------------------------------------------------------------------
2701 - className (string)
2702 - content (HTML string or jQuery element set)
2706 - right (the x coord of where the right edge should be. not a "CSS" right)
2707 - autoHide (boolean)
2712 var Popover
= Class
.extend(ListenerMixin
, {
2716 el
: null, // the container element for the popover. generated by this object
2717 margin
: 10, // the space required between the popover and the edges of the scroll container
2720 constructor: function(options
) {
2721 this.options
= options
|| {};
2725 // Shows the popover on the specified position. Renders it if not already
2727 if (this.isHidden
) {
2733 this.isHidden
= false;
2734 this.trigger('show');
2739 // Hides the popover, through CSS, but does not remove it from the DOM
2741 if (!this.isHidden
) {
2743 this.isHidden
= true;
2744 this.trigger('hide');
2749 // Creates `this.el` and renders content inside of it
2750 render: function() {
2752 var options
= this.options
;
2754 this.el
= $('<div class="fc-popover"/>')
2755 .addClass(options
.className
|| '')
2757 // position initially to the top left to avoid creating scrollbars
2761 .append(options
.content
)
2762 .appendTo(options
.parentEl
);
2764 // when a click happens on anything inside with a 'fc-close' className, hide the popover
2765 this.el
.on('click', '.fc-close', function() {
2769 if (options
.autoHide
) {
2770 this.listenTo($(document
), 'mousedown', this.documentMousedown
);
2775 // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
2776 documentMousedown: function(ev
) {
2777 // only hide the popover if the click happened outside the popover
2778 if (this.el
&& !$(ev
.target
).closest(this.el
).length
) {
2784 // Hides and unregisters any handlers
2785 removeElement: function() {
2793 this.stopListeningTo($(document
), 'mousedown');
2797 // Positions the popover optimally, using the top/left/right options
2798 position: function() {
2799 var options
= this.options
;
2800 var origin
= this.el
.offsetParent().offset();
2801 var width
= this.el
.outerWidth();
2802 var height
= this.el
.outerHeight();
2803 var windowEl
= $(window
);
2804 var viewportEl
= getScrollParent(this.el
);
2808 var top
; // the "position" (not "offset") values for the popover
2811 // compute top and left
2812 top
= options
.top
|| 0;
2813 if (options
.left
!== undefined) {
2814 left
= options
.left
;
2816 else if (options
.right
!== undefined) {
2817 left
= options
.right
- width
; // derive the left value from the right value
2823 if (viewportEl
.is(window
) || viewportEl
.is(document
)) { // normalize getScrollParent's result
2824 viewportEl
= windowEl
;
2825 viewportTop
= 0; // the window is always at the top left
2826 viewportLeft
= 0; // (and .offset() won't work if called here)
2829 viewportOffset
= viewportEl
.offset();
2830 viewportTop
= viewportOffset
.top
;
2831 viewportLeft
= viewportOffset
.left
;
2834 // if the window is scrolled, it causes the visible area to be further down
2835 viewportTop
+= windowEl
.scrollTop();
2836 viewportLeft
+= windowEl
.scrollLeft();
2838 // constrain to the view port. if constrained by two edges, give precedence to top/left
2839 if (options
.viewportConstrain
!== false) {
2840 top
= Math
.min(top
, viewportTop
+ viewportEl
.outerHeight() - height
- this.margin
);
2841 top
= Math
.max(top
, viewportTop
+ this.margin
);
2842 left
= Math
.min(left
, viewportLeft
+ viewportEl
.outerWidth() - width
- this.margin
);
2843 left
= Math
.max(left
, viewportLeft
+ this.margin
);
2847 top
: top
- origin
.top
,
2848 left
: left
- origin
.left
2853 // Triggers a callback. Calls a function in the option hash of the same name.
2854 // Arguments beyond the first `name` are forwarded on.
2855 // TODO: better code reuse for this. Repeat code
2856 trigger: function(name
) {
2857 if (this.options
[name
]) {
2858 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
2867 A cache for the left/right/top/bottom/width/height values for one or more elements.
2868 Works with both offset (from topleft document) and position (from offsetParent).
2875 var CoordCache
= FC
.CoordCache
= Class
.extend({
2877 els
: null, // jQuery set (assumed to be siblings)
2878 forcedOffsetParentEl
: null, // options can override the natural offsetParent
2879 origin
: null, // {left,top} position of offsetParent of els
2880 boundingRect
: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
2881 isHorizontal
: false, // whether to query for left/right/width
2882 isVertical
: false, // whether to query for top/bottom/height
2884 // arrays of coordinates (offsets from topleft of document)
2891 constructor: function(options
) {
2892 this.els
= $(options
.els
);
2893 this.isHorizontal
= options
.isHorizontal
;
2894 this.isVertical
= options
.isVertical
;
2895 this.forcedOffsetParentEl
= options
.offsetParent
? $(options
.offsetParent
) : null;
2899 // Queries the els for coordinates and stores them.
2900 // Call this method before using and of the get* methods below.
2902 var offsetParentEl
= this.forcedOffsetParentEl
;
2903 if (!offsetParentEl
&& this.els
.length
> 0) {
2904 offsetParentEl
= this.els
.eq(0).offsetParent();
2907 this.origin
= offsetParentEl
?
2908 offsetParentEl
.offset() :
2911 this.boundingRect
= this.queryBoundingRect();
2913 if (this.isHorizontal
) {
2914 this.buildElHorizontals();
2916 if (this.isVertical
) {
2917 this.buildElVerticals();
2922 // Destroys all internal data about coordinates, freeing memory
2925 this.boundingRect
= null;
2929 this.bottoms
= null;
2933 // When called, if coord caches aren't built, builds them
2934 ensureBuilt: function() {
2941 // Populates the left/right internal coordinate arrays
2942 buildElHorizontals: function() {
2946 this.els
.each(function(i
, node
) {
2948 var left
= el
.offset().left
;
2949 var width
= el
.outerWidth();
2952 rights
.push(left
+ width
);
2956 this.rights
= rights
;
2960 // Populates the top/bottom internal coordinate arrays
2961 buildElVerticals: function() {
2965 this.els
.each(function(i
, node
) {
2967 var top
= el
.offset().top
;
2968 var height
= el
.outerHeight();
2971 bottoms
.push(top
+ height
);
2975 this.bottoms
= bottoms
;
2979 // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
2980 // If no intersection is made, returns undefined.
2981 getHorizontalIndex: function(leftOffset
) {
2984 var lefts
= this.lefts
;
2985 var rights
= this.rights
;
2986 var len
= lefts
.length
;
2989 for (i
= 0; i
< len
; i
++) {
2990 if (leftOffset
>= lefts
[i
] && leftOffset
< rights
[i
]) {
2997 // Given a top offset (from document top), returns the index of the el that it vertically intersects.
2998 // If no intersection is made, returns undefined.
2999 getVerticalIndex: function(topOffset
) {
3002 var tops
= this.tops
;
3003 var bottoms
= this.bottoms
;
3004 var len
= tops
.length
;
3007 for (i
= 0; i
< len
; i
++) {
3008 if (topOffset
>= tops
[i
] && topOffset
< bottoms
[i
]) {
3015 // Gets the left offset (from document left) of the element at the given index
3016 getLeftOffset: function(leftIndex
) {
3018 return this.lefts
[leftIndex
];
3022 // Gets the left position (from offsetParent left) of the element at the given index
3023 getLeftPosition: function(leftIndex
) {
3025 return this.lefts
[leftIndex
] - this.origin
.left
;
3029 // Gets the right offset (from document left) of the element at the given index.
3030 // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
3031 getRightOffset: function(leftIndex
) {
3033 return this.rights
[leftIndex
];
3037 // Gets the right position (from offsetParent left) of the element at the given index.
3038 // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
3039 getRightPosition: function(leftIndex
) {
3041 return this.rights
[leftIndex
] - this.origin
.left
;
3045 // Gets the width of the element at the given index
3046 getWidth: function(leftIndex
) {
3048 return this.rights
[leftIndex
] - this.lefts
[leftIndex
];
3052 // Gets the top offset (from document top) of the element at the given index
3053 getTopOffset: function(topIndex
) {
3055 return this.tops
[topIndex
];
3059 // Gets the top position (from offsetParent top) of the element at the given position
3060 getTopPosition: function(topIndex
) {
3062 return this.tops
[topIndex
] - this.origin
.top
;
3065 // Gets the bottom offset (from the document top) of the element at the given index.
3066 // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3067 getBottomOffset: function(topIndex
) {
3069 return this.bottoms
[topIndex
];
3073 // Gets the bottom position (from the offsetParent top) of the element at the given index.
3074 // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3075 getBottomPosition: function(topIndex
) {
3077 return this.bottoms
[topIndex
] - this.origin
.top
;
3081 // Gets the height of the element at the given index
3082 getHeight: function(topIndex
) {
3084 return this.bottoms
[topIndex
] - this.tops
[topIndex
];
3089 // TODO: decouple this from CoordCache
3091 // Compute and return what the elements' bounding rectangle is, from the user's perspective.
3092 // Right now, only returns a rectangle if constrained by an overflow:scroll element.
3093 // Returns null if there are no elements
3094 queryBoundingRect: function() {
3097 if (this.els
.length
> 0) {
3098 scrollParentEl
= getScrollParent(this.els
.eq(0));
3100 if (!scrollParentEl
.is(document
)) {
3101 return getClientRect(scrollParentEl
);
3108 isPointInBounds: function(leftOffset
, topOffset
) {
3109 return this.isLeftInBounds(leftOffset
) && this.isTopInBounds(topOffset
);
3112 isLeftInBounds: function(leftOffset
) {
3113 return !this.boundingRect
|| (leftOffset
>= this.boundingRect
.left
&& leftOffset
< this.boundingRect
.right
);
3116 isTopInBounds: function(topOffset
) {
3117 return !this.boundingRect
|| (topOffset
>= this.boundingRect
.top
&& topOffset
< this.boundingRect
.bottom
);
3124 /* Tracks a drag's mouse movement, firing various handlers
3125 ----------------------------------------------------------------------------------------------------------------------*/
3126 // TODO: use Emitter
3128 var DragListener
= FC
.DragListener
= Class
.extend(ListenerMixin
, {
3133 // coordinates of the initial mousedown
3137 // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
3138 // TODO: do this for wrappers that have overflow:hidden as well.
3141 isInteracting
: false,
3142 isDistanceSurpassed
: false,
3143 isDelayEnded
: false,
3146 isGeneric
: false, // initiated by 'dragstart' (jqui)
3149 delayTimeoutId
: null,
3152 shouldCancelTouchScroll
: true,
3153 scrollAlwaysKills
: false,
3156 constructor: function(options
) {
3157 this.options
= options
|| {};
3161 // Interaction (high-level)
3162 // -----------------------------------------------------------------------------------------------------------------
3165 startInteraction: function(ev
, extraOptions
) {
3167 if (ev
.type
=== 'mousedown') {
3168 if (GlobalEmitter
.get().shouldIgnoreMouse()) {
3171 else if (!isPrimaryMouseButton(ev
)) {
3175 ev
.preventDefault(); // prevents native selection in most browsers
3179 if (!this.isInteracting
) {
3182 extraOptions
= extraOptions
|| {};
3183 this.delay
= firstDefined(extraOptions
.delay
, this.options
.delay
, 0);
3184 this.minDistance
= firstDefined(extraOptions
.distance
, this.options
.distance
, 0);
3185 this.subjectEl
= this.options
.subjectEl
;
3187 preventSelection($('body'));
3189 this.isInteracting
= true;
3190 this.isTouch
= getEvIsTouch(ev
);
3191 this.isGeneric
= ev
.type
=== 'dragstart';
3192 this.isDelayEnded
= false;
3193 this.isDistanceSurpassed
= false;
3195 this.originX
= getEvX(ev
);
3196 this.originY
= getEvY(ev
);
3197 this.scrollEl
= getScrollParent($(ev
.target
));
3199 this.bindHandlers();
3200 this.initAutoScroll();
3201 this.handleInteractionStart(ev
);
3202 this.startDelay(ev
);
3204 if (!this.minDistance
) {
3205 this.handleDistanceSurpassed(ev
);
3211 handleInteractionStart: function(ev
) {
3212 this.trigger('interactionStart', ev
);
3216 endInteraction: function(ev
, isCancelled
) {
3217 if (this.isInteracting
) {
3220 if (this.delayTimeoutId
) {
3221 clearTimeout(this.delayTimeoutId
);
3222 this.delayTimeoutId
= null;
3225 this.destroyAutoScroll();
3226 this.unbindHandlers();
3228 this.isInteracting
= false;
3229 this.handleInteractionEnd(ev
, isCancelled
);
3231 allowSelection($('body'));
3236 handleInteractionEnd: function(ev
, isCancelled
) {
3237 this.trigger('interactionEnd', ev
, isCancelled
|| false);
3242 // -----------------------------------------------------------------------------------------------------------------
3245 bindHandlers: function() {
3246 // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
3247 // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
3248 var globalEmitter
= GlobalEmitter
.get();
3250 if (this.isGeneric
) {
3251 this.listenTo($(document
), { // might only work on iOS because of GlobalEmitter's bind :(
3252 drag
: this.handleMove
,
3253 dragstop
: this.endInteraction
3256 else if (this.isTouch
) {
3257 this.listenTo(globalEmitter
, {
3258 touchmove
: this.handleTouchMove
,
3259 touchend
: this.endInteraction
,
3260 scroll
: this.handleTouchScroll
3264 this.listenTo(globalEmitter
, {
3265 mousemove
: this.handleMouseMove
,
3266 mouseup
: this.endInteraction
3270 this.listenTo(globalEmitter
, {
3271 selectstart
: preventDefault
, // don't allow selection while dragging
3272 contextmenu
: preventDefault
// long taps would open menu on Chrome dev tools
3277 unbindHandlers: function() {
3278 this.stopListeningTo(GlobalEmitter
.get());
3279 this.stopListeningTo($(document
)); // for isGeneric
3283 // Drag (high-level)
3284 // -----------------------------------------------------------------------------------------------------------------
3287 // extraOptions ignored if drag already started
3288 startDrag: function(ev
, extraOptions
) {
3289 this.startInteraction(ev
, extraOptions
); // ensure interaction began
3291 if (!this.isDragging
) {
3292 this.isDragging
= true;
3293 this.handleDragStart(ev
);
3298 handleDragStart: function(ev
) {
3299 this.trigger('dragStart', ev
);
3303 handleMove: function(ev
) {
3304 var dx
= getEvX(ev
) - this.originX
;
3305 var dy
= getEvY(ev
) - this.originY
;
3306 var minDistance
= this.minDistance
;
3307 var distanceSq
; // current distance from the origin, squared
3309 if (!this.isDistanceSurpassed
) {
3310 distanceSq
= dx
* dx
+ dy
* dy
;
3311 if (distanceSq
>= minDistance
* minDistance
) { // use pythagorean theorem
3312 this.handleDistanceSurpassed(ev
);
3316 if (this.isDragging
) {
3317 this.handleDrag(dx
, dy
, ev
);
3322 // Called while the mouse is being moved and when we know a legitimate drag is taking place
3323 handleDrag: function(dx
, dy
, ev
) {
3324 this.trigger('drag', dx
, dy
, ev
);
3325 this.updateAutoScroll(ev
); // will possibly cause scrolling
3329 endDrag: function(ev
) {
3330 if (this.isDragging
) {
3331 this.isDragging
= false;
3332 this.handleDragEnd(ev
);
3337 handleDragEnd: function(ev
) {
3338 this.trigger('dragEnd', ev
);
3343 // -----------------------------------------------------------------------------------------------------------------
3346 startDelay: function(initialEv
) {
3350 this.delayTimeoutId
= setTimeout(function() {
3351 _this
.handleDelayEnd(initialEv
);
3355 this.handleDelayEnd(initialEv
);
3360 handleDelayEnd: function(initialEv
) {
3361 this.isDelayEnded
= true;
3363 if (this.isDistanceSurpassed
) {
3364 this.startDrag(initialEv
);
3370 // -----------------------------------------------------------------------------------------------------------------
3373 handleDistanceSurpassed: function(ev
) {
3374 this.isDistanceSurpassed
= true;
3376 if (this.isDelayEnded
) {
3383 // -----------------------------------------------------------------------------------------------------------------
3386 handleTouchMove: function(ev
) {
3388 // prevent inertia and touchmove-scrolling while dragging
3389 if (this.isDragging
&& this.shouldCancelTouchScroll
) {
3390 ev
.preventDefault();
3393 this.handleMove(ev
);
3397 handleMouseMove: function(ev
) {
3398 this.handleMove(ev
);
3402 // Scrolling (unrelated to auto-scroll)
3403 // -----------------------------------------------------------------------------------------------------------------
3406 handleTouchScroll: function(ev
) {
3407 // if the drag is being initiated by touch, but a scroll happens before
3408 // the drag-initiating delay is over, cancel the drag
3409 if (!this.isDragging
|| this.scrollAlwaysKills
) {
3410 this.endInteraction(ev
, true); // isCancelled=true
3416 // -----------------------------------------------------------------------------------------------------------------
3419 // Triggers a callback. Calls a function in the option hash of the same name.
3420 // Arguments beyond the first `name` are forwarded on.
3421 trigger: function(name
) {
3422 if (this.options
[name
]) {
3423 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
3425 // makes _methods callable by event name. TODO: kill this
3426 if (this['_' + name
]) {
3427 this['_' + name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
3436 this.scrollEl is set in DragListener
3438 DragListener
.mixin({
3440 isAutoScroll
: false,
3442 scrollBounds
: null, // { top, bottom, left, right }
3443 scrollTopVel
: null, // pixels per second
3444 scrollLeftVel
: null, // pixels per second
3445 scrollIntervalId
: null, // ID of setTimeout for scrolling animation loop
3448 scrollSensitivity
: 30, // pixels from edge for scrolling to start
3449 scrollSpeed
: 200, // pixels per second, at maximum speed
3450 scrollIntervalMs
: 50, // millisecond wait between scroll increment
3453 initAutoScroll: function() {
3454 var scrollEl
= this.scrollEl
;
3457 this.options
.scroll
&&
3459 !scrollEl
.is(window
) &&
3460 !scrollEl
.is(document
);
3462 if (this.isAutoScroll
) {
3463 // debounce makes sure rapid calls don't happen
3464 this.listenTo(scrollEl
, 'scroll', debounce(this.handleDebouncedScroll
, 100));
3469 destroyAutoScroll: function() {
3470 this.endAutoScroll(); // kill any animation loop
3472 // remove the scroll handler if there is a scrollEl
3473 if (this.isAutoScroll
) {
3474 this.stopListeningTo(this.scrollEl
, 'scroll'); // will probably get removed by unbindHandlers too :(
3479 // Computes and stores the bounding rectangle of scrollEl
3480 computeScrollBounds: function() {
3481 if (this.isAutoScroll
) {
3482 this.scrollBounds
= getOuterRect(this.scrollEl
);
3483 // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
3488 // Called when the dragging is in progress and scrolling should be updated
3489 updateAutoScroll: function(ev
) {
3490 var sensitivity
= this.scrollSensitivity
;
3491 var bounds
= this.scrollBounds
;
3492 var topCloseness
, bottomCloseness
;
3493 var leftCloseness
, rightCloseness
;
3497 if (bounds
) { // only scroll if scrollEl exists
3499 // compute closeness to edges. valid range is from 0.0 - 1.0
3500 topCloseness
= (sensitivity
- (getEvY(ev
) - bounds
.top
)) / sensitivity
;
3501 bottomCloseness
= (sensitivity
- (bounds
.bottom
- getEvY(ev
))) / sensitivity
;
3502 leftCloseness
= (sensitivity
- (getEvX(ev
) - bounds
.left
)) / sensitivity
;
3503 rightCloseness
= (sensitivity
- (bounds
.right
- getEvX(ev
))) / sensitivity
;
3505 // translate vertical closeness into velocity.
3506 // mouse must be completely in bounds for velocity to happen.
3507 if (topCloseness
>= 0 && topCloseness
<= 1) {
3508 topVel
= topCloseness
* this.scrollSpeed
* -1; // negative. for scrolling up
3510 else if (bottomCloseness
>= 0 && bottomCloseness
<= 1) {
3511 topVel
= bottomCloseness
* this.scrollSpeed
;
3514 // translate horizontal closeness into velocity
3515 if (leftCloseness
>= 0 && leftCloseness
<= 1) {
3516 leftVel
= leftCloseness
* this.scrollSpeed
* -1; // negative. for scrolling left
3518 else if (rightCloseness
>= 0 && rightCloseness
<= 1) {
3519 leftVel
= rightCloseness
* this.scrollSpeed
;
3523 this.setScrollVel(topVel
, leftVel
);
3527 // Sets the speed-of-scrolling for the scrollEl
3528 setScrollVel: function(topVel
, leftVel
) {
3530 this.scrollTopVel
= topVel
;
3531 this.scrollLeftVel
= leftVel
;
3533 this.constrainScrollVel(); // massages into realistic values
3535 // if there is non-zero velocity, and an animation loop hasn't already started, then START
3536 if ((this.scrollTopVel
|| this.scrollLeftVel
) && !this.scrollIntervalId
) {
3537 this.scrollIntervalId
= setInterval(
3538 proxy(this, 'scrollIntervalFunc'), // scope to `this`
3539 this.scrollIntervalMs
3545 // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
3546 constrainScrollVel: function() {
3547 var el
= this.scrollEl
;
3549 if (this.scrollTopVel
< 0) { // scrolling up?
3550 if (el
.scrollTop() <= 0) { // already scrolled all the way up?
3551 this.scrollTopVel
= 0;
3554 else if (this.scrollTopVel
> 0) { // scrolling down?
3555 if (el
.scrollTop() + el
[0].clientHeight
>= el
[0].scrollHeight
) { // already scrolled all the way down?
3556 this.scrollTopVel
= 0;
3560 if (this.scrollLeftVel
< 0) { // scrolling left?
3561 if (el
.scrollLeft() <= 0) { // already scrolled all the left?
3562 this.scrollLeftVel
= 0;
3565 else if (this.scrollLeftVel
> 0) { // scrolling right?
3566 if (el
.scrollLeft() + el
[0].clientWidth
>= el
[0].scrollWidth
) { // already scrolled all the way right?
3567 this.scrollLeftVel
= 0;
3573 // This function gets called during every iteration of the scrolling animation loop
3574 scrollIntervalFunc: function() {
3575 var el
= this.scrollEl
;
3576 var frac
= this.scrollIntervalMs
/ 1000; // considering animation frequency, what the vel should be mult'd by
3578 // change the value of scrollEl's scroll
3579 if (this.scrollTopVel
) {
3580 el
.scrollTop(el
.scrollTop() + this.scrollTopVel
* frac
);
3582 if (this.scrollLeftVel
) {
3583 el
.scrollLeft(el
.scrollLeft() + this.scrollLeftVel
* frac
);
3586 this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
3588 // if scrolled all the way, which causes the vels to be zero, stop the animation loop
3589 if (!this.scrollTopVel
&& !this.scrollLeftVel
) {
3590 this.endAutoScroll();
3595 // Kills any existing scrolling animation loop
3596 endAutoScroll: function() {
3597 if (this.scrollIntervalId
) {
3598 clearInterval(this.scrollIntervalId
);
3599 this.scrollIntervalId
= null;
3601 this.handleScrollEnd();
3606 // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
3607 handleDebouncedScroll: function() {
3608 // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
3609 if (!this.scrollIntervalId
) {
3610 this.handleScrollEnd();
3615 // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3616 handleScrollEnd: function() {
3622 /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
3623 ------------------------------------------------------------------------------------------------------------------------
3629 var HitDragListener
= DragListener
.extend({
3631 component
: null, // converts coordinates to hits
3632 // methods: hitsNeeded, hitsNotNeeded, queryHit
3634 origHit
: null, // the hit the mouse was over when listening started
3635 hit
: null, // the hit the mouse is over
3636 coordAdjust
: null, // delta that will be added to the mouse coordinates when computing collisions
3639 constructor: function(component
, options
) {
3640 DragListener
.call(this, options
); // call the super-constructor
3642 this.component
= component
;
3646 // Called when drag listening starts (but a real drag has not necessarily began).
3647 // ev might be undefined if dragging was started manually.
3648 handleInteractionStart: function(ev
) {
3649 var subjectEl
= this.subjectEl
;
3654 this.component
.hitsNeeded();
3655 this.computeScrollBounds(); // for autoscroll
3658 origPoint
= { left
: getEvX(ev
), top
: getEvY(ev
) };
3661 // constrain the point to bounds of the element being dragged
3663 subjectRect
= getOuterRect(subjectEl
); // used for centering as well
3664 point
= constrainPoint(point
, subjectRect
);
3667 this.origHit
= this.queryHit(point
.left
, point
.top
);
3669 // treat the center of the subject as the collision point?
3670 if (subjectEl
&& this.options
.subjectCenter
) {
3672 // only consider the area the subject overlaps the hit. best for large subjects.
3673 // TODO: skip this if hit didn't supply left/right/top/bottom
3675 subjectRect
= intersectRects(this.origHit
, subjectRect
) ||
3676 subjectRect
; // in case there is no intersection
3679 point
= getRectCenter(subjectRect
);
3682 this.coordAdjust
= diffPoints(point
, origPoint
); // point - origPoint
3685 this.origHit
= null;
3686 this.coordAdjust
= null;
3689 // call the super-method. do it after origHit has been computed
3690 DragListener
.prototype.handleInteractionStart
.apply(this, arguments
);
3694 // Called when the actual drag has started
3695 handleDragStart: function(ev
) {
3698 DragListener
.prototype.handleDragStart
.apply(this, arguments
); // call the super-method
3700 // might be different from this.origHit if the min-distance is large
3701 hit
= this.queryHit(getEvX(ev
), getEvY(ev
));
3703 // report the initial hit the mouse is over
3704 // especially important if no min-distance and drag starts immediately
3706 this.handleHitOver(hit
);
3711 // Called when the drag moves
3712 handleDrag: function(dx
, dy
, ev
) {
3715 DragListener
.prototype.handleDrag
.apply(this, arguments
); // call the super-method
3717 hit
= this.queryHit(getEvX(ev
), getEvY(ev
));
3719 if (!isHitsEqual(hit
, this.hit
)) { // a different hit than before?
3721 this.handleHitOut();
3724 this.handleHitOver(hit
);
3730 // Called when dragging has been stopped
3731 handleDragEnd: function() {
3732 this.handleHitDone();
3733 DragListener
.prototype.handleDragEnd
.apply(this, arguments
); // call the super-method
3737 // Called when a the mouse has just moved over a new hit
3738 handleHitOver: function(hit
) {
3739 var isOrig
= isHitsEqual(hit
, this.origHit
);
3743 this.trigger('hitOver', this.hit
, isOrig
, this.origHit
);
3747 // Called when the mouse has just moved out of a hit
3748 handleHitOut: function() {
3750 this.trigger('hitOut', this.hit
);
3751 this.handleHitDone();
3757 // Called after a hitOut. Also called before a dragStop
3758 handleHitDone: function() {
3760 this.trigger('hitDone', this.hit
);
3765 // Called when the interaction ends, whether there was a real drag or not
3766 handleInteractionEnd: function() {
3767 DragListener
.prototype.handleInteractionEnd
.apply(this, arguments
); // call the super-method
3769 this.origHit
= null;
3772 this.component
.hitsNotNeeded();
3776 // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3777 handleScrollEnd: function() {
3778 DragListener
.prototype.handleScrollEnd
.apply(this, arguments
); // call the super-method
3780 // hits' absolute positions will be in new places after a user's scroll.
3781 // HACK for recomputing.
3782 if (this.isDragging
) {
3783 this.component
.releaseHits();
3784 this.component
.prepareHits();
3789 // Gets the hit underneath the coordinates for the given mouse event
3790 queryHit: function(left
, top
) {
3792 if (this.coordAdjust
) {
3793 left
+= this.coordAdjust
.left
;
3794 top
+= this.coordAdjust
.top
;
3797 return this.component
.queryHit(left
, top
);
3803 // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
3804 // Two null values will be considered equal, as two "out of the component" states are the same.
3805 function isHitsEqual(hit0
, hit1
) {
3807 if (!hit0
&& !hit1
) {
3812 return hit0
.component
=== hit1
.component
&&
3813 isHitPropsWithin(hit0
, hit1
) &&
3814 isHitPropsWithin(hit1
, hit0
); // ensures all props are identical
3821 // Returns true if all of subHit's non-standard properties are within superHit
3822 function isHitPropsWithin(subHit
, superHit
) {
3823 for (var propName
in subHit
) {
3824 if (!/^(component|left|right|top|bottom)$/.test(propName
)) {
3825 if (subHit
[propName
] !== superHit
[propName
]) {
3836 Listens to document and window-level user-interaction events, like touch events and mouse events,
3837 and fires these events as-is to whoever is observing a GlobalEmitter.
3838 Best when used as a singleton via GlobalEmitter.get()
3840 Normalizes mouse/touch events. For examples:
3841 - ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
3842 - compensates for various buggy scenarios where a touchend does not fire
3845 FC
.touchMouseIgnoreWait
= 500;
3847 var GlobalEmitter
= Class
.extend(ListenerMixin
, EmitterMixin
, {
3850 mouseIgnoreDepth
: 0,
3851 handleScrollProxy
: null,
3857 this.listenTo($(document
), {
3858 touchstart
: this.handleTouchStart
,
3859 touchcancel
: this.handleTouchCancel
,
3860 touchend
: this.handleTouchEnd
,
3861 mousedown
: this.handleMouseDown
,
3862 mousemove
: this.handleMouseMove
,
3863 mouseup
: this.handleMouseUp
,
3864 click
: this.handleClick
,
3865 selectstart
: this.handleSelectStart
,
3866 contextmenu
: this.handleContextMenu
3869 // because we need to call preventDefault
3870 // because https://www.chromestatus.com/features/5093566007214080
3871 // TODO: investigate performance because this is a global handler
3872 window
.addEventListener(
3874 this.handleTouchMoveProxy = function(ev
) {
3875 _this
.handleTouchMove($.Event(ev
));
3877 { passive
: false } // allows preventDefault()
3880 // attach a handler to get called when ANY scroll action happens on the page.
3881 // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
3882 // http://stackoverflow.com/a/32954565/96342
3883 window
.addEventListener(
3885 this.handleScrollProxy = function(ev
) {
3886 _this
.handleScroll($.Event(ev
));
3892 unbind: function() {
3893 this.stopListeningTo($(document
));
3895 window
.removeEventListener(
3897 this.handleTouchMoveProxy
3900 window
.removeEventListener(
3902 this.handleScrollProxy
,
3909 // -----------------------------------------------------------------------------------------------------------------
3911 handleTouchStart: function(ev
) {
3913 // if a previous touch interaction never ended with a touchend, then implicitly end it,
3914 // but since a new touch interaction is about to begin, don't start the mouse ignore period.
3915 this.stopTouch(ev
, true); // skipMouseIgnore=true
3917 this.isTouching
= true;
3918 this.trigger('touchstart', ev
);
3921 handleTouchMove: function(ev
) {
3922 if (this.isTouching
) {
3923 this.trigger('touchmove', ev
);
3927 handleTouchCancel: function(ev
) {
3928 if (this.isTouching
) {
3929 this.trigger('touchcancel', ev
);
3931 // Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both.
3932 // If touchend fires later, it won't have any effect b/c isTouching will be false.
3937 handleTouchEnd: function(ev
) {
3943 // -----------------------------------------------------------------------------------------------------------------
3945 handleMouseDown: function(ev
) {
3946 if (!this.shouldIgnoreMouse()) {
3947 this.trigger('mousedown', ev
);
3951 handleMouseMove: function(ev
) {
3952 if (!this.shouldIgnoreMouse()) {
3953 this.trigger('mousemove', ev
);
3957 handleMouseUp: function(ev
) {
3958 if (!this.shouldIgnoreMouse()) {
3959 this.trigger('mouseup', ev
);
3963 handleClick: function(ev
) {
3964 if (!this.shouldIgnoreMouse()) {
3965 this.trigger('click', ev
);
3971 // -----------------------------------------------------------------------------------------------------------------
3973 handleSelectStart: function(ev
) {
3974 this.trigger('selectstart', ev
);
3977 handleContextMenu: function(ev
) {
3978 this.trigger('contextmenu', ev
);
3981 handleScroll: function(ev
) {
3982 this.trigger('scroll', ev
);
3987 // -----------------------------------------------------------------------------------------------------------------
3989 stopTouch: function(ev
, skipMouseIgnore
) {
3990 if (this.isTouching
) {
3991 this.isTouching
= false;
3992 this.trigger('touchend', ev
);
3994 if (!skipMouseIgnore
) {
3995 this.startTouchMouseIgnore();
4000 startTouchMouseIgnore: function() {
4002 var wait
= FC
.touchMouseIgnoreWait
;
4005 this.mouseIgnoreDepth
++;
4006 setTimeout(function() {
4007 _this
.mouseIgnoreDepth
--;
4012 shouldIgnoreMouse: function() {
4013 return this.isTouching
|| Boolean(this.mouseIgnoreDepth
);
4020 // ---------------------------------------------------------------------------------------------------------------------
4023 var globalEmitter
= null;
4024 var neededCount
= 0;
4027 // gets the singleton
4028 GlobalEmitter
.get = function() {
4030 if (!globalEmitter
) {
4031 globalEmitter
= new GlobalEmitter();
4032 globalEmitter
.bind();
4035 return globalEmitter
;
4039 // called when an object knows it will need a GlobalEmitter in the near future.
4040 GlobalEmitter
.needed = function() {
4041 GlobalEmitter
.get(); // ensures globalEmitter
4046 // called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
4047 GlobalEmitter
.unneeded = function() {
4050 if (!neededCount
) { // nobody else needs it
4051 globalEmitter
.unbind();
4052 globalEmitter
= null;
4060 /* Creates a clone of an element and lets it track the mouse as it moves
4061 ----------------------------------------------------------------------------------------------------------------------*/
4063 var MouseFollower
= Class
.extend(ListenerMixin
, {
4067 sourceEl
: null, // the element that will be cloned and made to look like it is dragging
4068 el
: null, // the clone of `sourceEl` that will track the mouse
4069 parentEl
: null, // the element that `el` (the clone) will be attached to
4071 // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
4075 // the absolute coordinates of the initiating touch/mouse action
4079 // the number of pixels the mouse has moved from its initial position
4085 isAnimating
: false, // doing the revert animation?
4087 constructor: function(sourceEl
, options
) {
4088 this.options
= options
= options
|| {};
4089 this.sourceEl
= sourceEl
;
4090 this.parentEl
= options
.parentEl
? $(options
.parentEl
) : sourceEl
.parent(); // default to sourceEl's parent
4094 // Causes the element to start following the mouse
4095 start: function(ev
) {
4096 if (!this.isFollowing
) {
4097 this.isFollowing
= true;
4099 this.y0
= getEvY(ev
);
4100 this.x0
= getEvX(ev
);
4104 if (!this.isHidden
) {
4105 this.updatePosition();
4108 if (getEvIsTouch(ev
)) {
4109 this.listenTo($(document
), 'touchmove', this.handleMove
);
4112 this.listenTo($(document
), 'mousemove', this.handleMove
);
4118 // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
4119 // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
4120 stop: function(shouldRevert
, callback
) {
4122 var revertDuration
= this.options
.revertDuration
;
4124 function complete() { // might be called by .animate(), which might change `this` context
4125 _this
.isAnimating
= false;
4126 _this
.removeElement();
4128 _this
.top0
= _this
.left0
= null; // reset state for future updatePosition calls
4135 if (this.isFollowing
&& !this.isAnimating
) { // disallow more than one stop animation at a time
4136 this.isFollowing
= false;
4138 this.stopListeningTo($(document
));
4140 if (shouldRevert
&& revertDuration
&& !this.isHidden
) { // do a revert animation?
4141 this.isAnimating
= true;
4146 duration
: revertDuration
,
4157 // Gets the tracking element. Create it if necessary
4162 el
= this.el
= this.sourceEl
.clone()
4163 .addClass(this.options
.additionalClass
|| '')
4165 position
: 'absolute',
4166 visibility
: '', // in case original element was hidden (commonly through hideEvents())
4167 display
: this.isHidden
? 'none' : '', // for when initially hidden
4169 right
: 'auto', // erase and set width instead
4170 bottom
: 'auto', // erase and set height instead
4171 width
: this.sourceEl
.width(), // explicit height in case there was a 'right' value
4172 height
: this.sourceEl
.height(), // explicit width in case there was a 'bottom' value
4173 opacity
: this.options
.opacity
|| '',
4174 zIndex
: this.options
.zIndex
4177 // we don't want long taps or any mouse interaction causing selection/menus.
4178 // would use preventSelection(), but that prevents selectstart, causing problems.
4179 el
.addClass('fc-unselectable');
4181 el
.appendTo(this.parentEl
);
4188 // Removes the tracking element if it has already been created
4189 removeElement: function() {
4197 // Update the CSS position of the tracking element
4198 updatePosition: function() {
4202 this.getEl(); // ensure this.el
4204 // make sure origin info was computed
4205 if (this.top0
=== null) {
4206 sourceOffset
= this.sourceEl
.offset();
4207 origin
= this.el
.offsetParent().offset();
4208 this.top0
= sourceOffset
.top
- origin
.top
;
4209 this.left0
= sourceOffset
.left
- origin
.left
;
4213 top
: this.top0
+ this.topDelta
,
4214 left
: this.left0
+ this.leftDelta
4219 // Gets called when the user moves the mouse
4220 handleMove: function(ev
) {
4221 this.topDelta
= getEvY(ev
) - this.y0
;
4222 this.leftDelta
= getEvX(ev
) - this.x0
;
4224 if (!this.isHidden
) {
4225 this.updatePosition();
4230 // Temporarily makes the tracking element invisible. Can be called before following starts
4232 if (!this.isHidden
) {
4233 this.isHidden
= true;
4241 // Show the tracking element after it has been temporarily hidden
4243 if (this.isHidden
) {
4244 this.isHidden
= false;
4245 this.updatePosition();
4246 this.getEl().show();
4254 var ChronoComponent
= Model
.extend({
4258 el
: null, // the view's containing element. set by Calendar(?)
4260 // frequently accessed options
4262 nextDayThreshold
: null,
4265 constructor: function() {
4270 this.nextDayThreshold
= moment
.duration(this.opt('nextDayThreshold'));
4271 this.isRTL
= this.opt('isRTL');
4275 addChild: function(chronoComponent
) {
4276 this.children
.push(chronoComponent
);
4281 // -----------------------------------------------------------------------------------------------------------------
4284 opt: function(name
) {
4285 // subclasses must implement
4289 publiclyTrigger: function(/**/) {
4290 var calendar
= this._getCalendar();
4292 return calendar
.publiclyTrigger
.apply(calendar
, arguments
);
4296 hasPublicHandlers: function(/**/) {
4297 var calendar
= this._getCalendar();
4299 return calendar
.hasPublicHandlers
.apply(calendar
, arguments
);
4304 // -----------------------------------------------------------------------------------------------------------------
4307 // Sets the container element that the view should render inside of, does global DOM-related initializations,
4308 // and renders all the non-date-related content inside.
4309 setElement: function(el
) {
4311 this.bindGlobalHandlers();
4312 this.renderSkeleton();
4316 // Removes the view's container element from the DOM, clearing any content beforehand.
4317 // Undoes any other DOM-related attachments.
4318 removeElement: function() {
4319 this.unrenderSkeleton();
4320 this.unbindGlobalHandlers();
4323 // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
4324 // We don't null-out the View's other jQuery element references upon destroy,
4325 // so we shouldn't kill this.el either.
4329 bindGlobalHandlers: function() {
4333 unbindGlobalHandlers: function() {
4338 // -----------------------------------------------------------------------------------------------------------------
4341 // Renders the basic structure of the view before any content is rendered
4342 renderSkeleton: function() {
4343 // subclasses should implement
4347 // Unrenders the basic structure of the view
4348 unrenderSkeleton: function() {
4349 // subclasses should implement
4353 // Date Low-level Rendering
4354 // -----------------------------------------------------------------------------------------------------------------
4357 // date-cell content only
4358 renderDates: function() {
4359 // subclasses should implement
4363 // date-cell content only
4364 unrenderDates: function() {
4365 // subclasses should override
4370 // -----------------------------------------------------------------------------------------------------------------
4373 // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
4374 // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
4375 getNowIndicatorUnit: function() {
4376 // subclasses should implement
4380 // Renders a current time indicator at the given datetime
4381 renderNowIndicator: function(date
) {
4382 this.callChildren('renderNowIndicator', date
);
4386 // Undoes the rendering actions from renderNowIndicator
4387 unrenderNowIndicator: function() {
4388 this.callChildren('unrenderNowIndicator');
4393 // ---------------------------------------------------------------------------------------------------------------
4396 // Renders business-hours onto the view. Assumes updateSize has already been called.
4397 renderBusinessHours: function() {
4398 this.callChildren('renderBusinessHours');
4402 // Unrenders previously-rendered business-hours
4403 unrenderBusinessHours: function() {
4404 this.callChildren('unrenderBusinessHours');
4408 // Event Low-level Rendering
4409 // -----------------------------------------------------------------------------------------------------------------
4412 // Renders the events onto the view.
4413 // TODO: eventually rename to `renderEvents` once legacy is gone.
4414 renderEventsPayload: function(eventsPayload
) {
4415 this.callChildren('renderEventsPayload', eventsPayload
);
4419 // Removes event elements from the view.
4420 unrenderEvents: function() {
4421 this.callChildren('unrenderEvents');
4423 // we DON'T need to call updateHeight() because
4424 // a renderEventsPayload() call always happens after this, which will eventually call updateHeight()
4428 // Retrieves all segment objects that are rendered in the view
4429 getEventSegs: function() {
4430 var children
= this.children
;
4434 for (i
= 0; i
< children
.length
; i
++) {
4435 segs
.push
.apply( // append
4437 children
[i
].getEventSegs()
4445 // Drag-n-Drop Rendering (for both events and external elements)
4446 // ---------------------------------------------------------------------------------------------------------------
4449 // Renders a visual indication of a event or external-element drag over the given drop zone.
4450 // If an external-element, seg will be `null`.
4451 // Must return elements used for any mock events.
4452 renderDrag: function(eventFootprints
, seg
) {
4454 var children
= this.children
;
4458 for (i
= 0; i
< children
.length
; i
++) {
4459 childDragEls
= children
[i
].renderDrag(eventFootprints
, seg
);
4463 dragEls
= childDragEls
;
4466 dragEls
= dragEls
.add(childDragEls
);
4475 // Unrenders a visual indication of an event or external-element being dragged.
4476 unrenderDrag: function() {
4477 this.callChildren('unrenderDrag');
4482 // ---------------------------------------------------------------------------------------------------------------
4485 // Renders a visual indication of the selection
4486 // TODO: rename to `renderSelection` after legacy is gone
4487 renderSelectionFootprint: function(componentFootprint
) {
4488 this.callChildren('renderSelectionFootprint', componentFootprint
);
4492 // Unrenders a visual indication of selection
4493 unrenderSelection: function() {
4494 this.callChildren('unrenderSelection');
4499 // ---------------------------------------------------------------------------------------------------------------
4502 hitsNeeded: function() {
4503 this.callChildren('hitsNeeded');
4507 hitsNotNeeded: function() {
4508 this.callChildren('hitsNotNeeded');
4512 // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
4513 prepareHits: function() {
4514 this.callChildren('prepareHits');
4518 // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
4519 releaseHits: function() {
4520 this.callChildren('releaseHits');
4524 // Given coordinates from the topleft of the document, return data about the date-related area underneath.
4525 // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
4526 // Must have a `grid` property, a reference to this current grid. TODO: avoid this
4527 // The returned object will be processed by getHitFootprint and getHitEl.
4528 queryHit: function(leftOffset
, topOffset
) {
4529 var children
= this.children
;
4533 for (i
= 0; i
< children
.length
; i
++) {
4534 hit
= children
[i
].queryHit(leftOffset
, topOffset
);
4546 // Event Drag-n-Drop
4547 // ---------------------------------------------------------------------------------------------------------------
4550 // Computes if the given event is allowed to be dragged by the user
4551 isEventDefDraggable: function(eventDef
) {
4552 return this.isEventDefStartEditable(eventDef
);
4556 isEventDefStartEditable: function(eventDef
) {
4557 var isEditable
= eventDef
.isStartExplicitlyEditable();
4559 if (isEditable
== null) {
4560 isEditable
= this.opt('eventStartEditable');
4562 if (isEditable
== null) {
4563 isEditable
= this.isEventDefGenerallyEditable(eventDef
);
4571 isEventDefGenerallyEditable: function(eventDef
) {
4572 var isEditable
= eventDef
.isExplicitlyEditable();
4574 if (isEditable
== null) {
4575 isEditable
= this.opt('editable');
4583 // ---------------------------------------------------------------------------------------------------------------
4586 // Computes if the given event is allowed to be resized from its starting edge
4587 isEventDefResizableFromStart: function(eventDef
) {
4588 return this.opt('eventResizableFromStart') && this.isEventDefResizable(eventDef
);
4592 // Computes if the given event is allowed to be resized from its ending edge
4593 isEventDefResizableFromEnd: function(eventDef
) {
4594 return this.isEventDefResizable(eventDef
);
4598 // Computes if the given event is allowed to be resized by the user at all
4599 isEventDefResizable: function(eventDef
) {
4600 var isResizable
= eventDef
.isDurationExplicitlyEditable();
4602 if (isResizable
== null) {
4603 isResizable
= this.opt('eventDurationEditable');
4605 if (isResizable
== null) {
4606 isResizable
= this.isEventDefGenerallyEditable(eventDef
);
4613 // Foreground Segment Rendering
4614 // ---------------------------------------------------------------------------------------------------------------
4617 // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
4618 renderFgSegs: function(segs
) {
4619 // subclasses must implement
4623 // Unrenders all currently rendered foreground segments
4624 unrenderFgSegs: function() {
4625 // subclasses must implement
4629 // Renders and assigns an `el` property for each foreground event segment.
4630 // Only returns segments that successfully rendered.
4631 // A utility that subclasses may use.
4632 renderFgSegEls: function(segs
, disableResizing
) {
4634 var hasEventRenderHandlers
= this.hasPublicHandlers('eventRender');
4636 var renderedSegs
= [];
4639 if (segs
.length
) { // don't build an empty html string
4641 // build a large concatenation of event segment HTML
4642 for (i
= 0; i
< segs
.length
; i
++) {
4643 html
+= this.fgSegHtml(segs
[i
], disableResizing
);
4646 // Grab individual elements from the combined HTML string. Use each as the default rendering.
4647 // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4648 $(html
).each(function(i
, node
) {
4652 if (hasEventRenderHandlers
) { // optimization
4653 el
= _this
.filterEventRenderEl(seg
.footprint
, el
);
4657 el
.data('fc-seg', seg
); // used by handlers
4659 renderedSegs
.push(seg
);
4664 return renderedSegs
;
4668 // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
4669 fgSegHtml: function(seg
, disableResizing
) {
4670 // subclasses should implement
4674 // Given an event and the default element used for rendering, returns the element that should actually be used.
4675 // Basically runs events and elements through the eventRender hook.
4676 filterEventRenderEl: function(eventFootprint
, el
) {
4677 var legacy
= eventFootprint
.getEventLegacy();
4679 var custom
= this.publiclyTrigger('eventRender', {
4681 args
: [ legacy
, el
, this._getView() ]
4684 if (custom
=== false) { // means don't render at all
4687 else if (custom
&& custom
!== true) {
4696 // ----------------------------------------------------------------------------------------------------------------
4699 // Generates HTML for an anchor to another view into the calendar.
4700 // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
4701 // `gotoOptions` can either be a moment input, or an object with the form:
4702 // { date, type, forceOff }
4703 // `type` is a view-type like "day" or "week". default value is "day".
4704 // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
4705 buildGotoAnchorHtml: function(gotoOptions
, attrs
, innerHtml
) {
4706 var date
, type
, forceOff
;
4709 if ($.isPlainObject(gotoOptions
)) {
4710 date
= gotoOptions
.date
;
4711 type
= gotoOptions
.type
;
4712 forceOff
= gotoOptions
.forceOff
;
4715 date
= gotoOptions
; // a single moment input
4717 date
= FC
.moment(date
); // if a string, parse it
4719 finalOptions
= { // for serialization into the link
4720 date
: date
.format('YYYY-MM-DD'),
4724 if (typeof attrs
=== 'string') {
4729 attrs
= attrs
? ' ' + attrsToStr(attrs
) : ''; // will have a leading space
4730 innerHtml
= innerHtml
|| '';
4732 if (!forceOff
&& this.opt('navLinks')) {
4733 return '<a' + attrs
+
4734 ' data-goto="' + htmlEscape(JSON
.stringify(finalOptions
)) + '">' +
4739 return '<span' + attrs
+ '>' +
4746 // Date Formatting Utils
4747 // ---------------------------------------------------------------------------------------------------------------
4750 // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
4751 // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
4752 // The timezones of the dates within `range` will be respected.
4753 formatRange: function(range
, isAllDay
, formatStr
, separator
) {
4754 var end
= range
.end
;
4757 end
= end
.clone().subtract(1); // convert to inclusive. last ms of previous day
4760 return formatRange(range
.start
, end
, formatStr
, separator
, this.isRTL
);
4764 getAllDayHtml: function() {
4765 return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
4769 // Computes HTML classNames for a single-day element
4770 getDayClasses: function(date
, noThemeHighlight
) {
4771 var view
= this._getView();
4775 if (!view
.activeUnzonedRange
.containsDate(date
)) {
4776 classes
.push('fc-disabled-day'); // TODO: jQuery UI theme?
4779 classes
.push('fc-' + dayIDs
[date
.day()]);
4781 if (view
.isDateInOtherMonth(date
)) { // TODO: use ChronoComponent subclass somehow
4782 classes
.push('fc-other-month');
4785 today
= view
.calendar
.getNow();
4787 if (date
.isSame(today
, 'day')) {
4788 classes
.push('fc-today');
4790 if (noThemeHighlight
!== true) {
4791 classes
.push(view
.calendar
.theme
.getClass('today'));
4794 else if (date
< today
) {
4795 classes
.push('fc-past');
4798 classes
.push('fc-future');
4807 // ---------------------------------------------------------------------------------------------------------------
4810 // Returns the date range of the full days the given range visually appears to occupy.
4811 // Returns a plain object with start/end, NOT an UnzonedRange!
4812 computeDayRange: function(unzonedRange
) {
4813 var calendar
= this._getCalendar();
4814 var startDay
= calendar
.msToUtcMoment(unzonedRange
.startMs
, true); // the beginning of the day the range starts
4815 var end
= calendar
.msToUtcMoment(unzonedRange
.endMs
);
4816 var endTimeMS
= +end
.time(); // # of milliseconds into `endDay`
4817 var endDay
= end
.clone().stripTime(); // the beginning of the day the range exclusively ends
4819 // If the end time is actually inclusively part of the next day and is equal to or
4820 // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
4821 // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
4822 if (endTimeMS
&& endTimeMS
>= this.nextDayThreshold
) {
4823 endDay
.add(1, 'days');
4826 // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
4827 if (endDay
<= startDay
) {
4828 endDay
= startDay
.clone().add(1, 'days');
4831 return { start
: startDay
, end
: endDay
};
4835 // Does the given range visually appear to occupy more than one day?
4836 isMultiDayRange: function(unzonedRange
) {
4837 var dayRange
= this.computeDayRange(unzonedRange
);
4839 return dayRange
.end
.diff(dayRange
.start
, 'days') > 1;
4844 // ---------------------------------------------------------------------------------------------------------------
4847 callChildren: function(methodName
) {
4848 var args
= Array
.prototype.slice
.call(arguments
, 1);
4849 var children
= this.children
;
4852 for (i
= 0; i
< children
.length
; i
++) {
4853 child
= children
[i
];
4854 child
[methodName
].apply(child
, args
);
4859 _getCalendar: function() { // TODO: strip out. move to generic parent.
4860 return this.calendar
|| this.view
.calendar
;
4864 _getView: function() { // TODO: strip out. move to generic parent.
4872 /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
4873 ----------------------------------------------------------------------------------------------------------------------
4876 - range->footprint->seg pipeline
4877 - initializing day click
4878 - initializing selection system
4879 - initializing mouse/touch handlers for everything
4880 - initializing event rendering-related options
4883 var Grid
= FC
.Grid
= ChronoComponent
.extend({
4885 // self-config, overridable by subclasses
4886 hasDayInteractions
: true, // can user click/select ranges of time?
4888 view
: null, // a View object
4889 isRTL
: null, // shortcut to the view's isRTL option
4893 hitsNeededDepth
: 0, // necessary because multiple callers might need the same hits
4895 dayClickListener
: null,
4896 daySelectListener
: null,
4897 segDragListener
: null,
4898 segResizeListener
: null,
4899 externalDragListener
: null,
4902 constructor: function(view
) {
4905 ChronoComponent
.call(this);
4907 this.initFillInternals();
4909 this.dayClickListener
= this.buildDayClickListener();
4910 this.daySelectListener
= this.buildDaySelectListener();
4914 opt: function(name
) {
4915 return this.view
.opt(name
);
4920 ------------------------------------------------------------------------------------------------------------------*/
4923 // Tells the grid about what period of time to display.
4924 // Any date-related internal data should be generated.
4925 setRange: function(unzonedRange
) {
4926 this.unzonedRange
= unzonedRange
;
4928 this.rangeUpdated();
4929 this.processRangeOptions();
4933 // Called when internal variables that rely on the range should be updated
4934 rangeUpdated: function() {
4938 // Updates values that rely on options and also relate to range
4939 processRangeOptions: function() {
4940 var displayEventTime
;
4941 var displayEventEnd
;
4943 this.eventTimeFormat
= // for Grid.event-rendering.js
4944 this.opt('eventTimeFormat') ||
4945 this.opt('timeFormat') || // deprecated
4946 this.computeEventTimeFormat();
4948 displayEventTime
= this.opt('displayEventTime');
4949 if (displayEventTime
== null) {
4950 displayEventTime
= this.computeDisplayEventTime(); // might be based off of range
4953 displayEventEnd
= this.opt('displayEventEnd');
4954 if (displayEventEnd
== null) {
4955 displayEventEnd
= this.computeDisplayEventEnd(); // might be based off of range
4958 this.displayEventTime
= displayEventTime
;
4959 this.displayEventEnd
= displayEventEnd
;
4965 ------------------------------------------------------------------------------------------------------------------*/
4968 hitsNeeded: function() {
4969 if (!(this.hitsNeededDepth
++)) {
4975 hitsNotNeeded: function() {
4976 if (this.hitsNeededDepth
&& !(--this.hitsNeededDepth
)) {
4982 getSafeHitFootprint: function(hit
) {
4983 var footprint
= this.getHitFootprint(hit
);
4985 if (!this.view
.activeUnzonedRange
.containsRange(footprint
.unzonedRange
)) {
4993 getHitFootprint: function(hit
) {
4997 // Given position-level information about a date-related area within the grid,
4998 // should return a jQuery element that best represents it. passed to dayClick callback.
4999 getHitEl: function(hit
) {
5004 ------------------------------------------------------------------------------------------------------------------*/
5007 // Sets the container element that the grid should render inside of.
5008 // Does other DOM-related initializations.
5009 setElement: function(el
) {
5010 ChronoComponent
.prototype.setElement
.apply(this, arguments
);
5012 if (this.hasDayInteractions
) {
5013 preventSelection(el
);
5015 this.bindDayHandler('touchstart', this.dayTouchStart
);
5016 this.bindDayHandler('mousedown', this.dayMousedown
);
5019 // attach event-element-related handlers. in Grid.events
5020 // same garbage collection note as above.
5021 this.bindSegHandlers();
5025 bindDayHandler: function(name
, handler
) {
5028 // attach a handler to the grid's root element.
5029 // jQuery will take care of unregistering them when removeElement gets called.
5030 this.el
.on(name
, function(ev
) {
5033 _this
.segSelector
+ ',' + // directly on an event element
5034 _this
.segSelector
+ ' *,' + // within an event element
5035 '.fc-more,' + // a "more.." link
5036 'a[data-goto]' // a clickable nav link
5039 return handler
.call(_this
, ev
);
5045 // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
5046 // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
5047 removeElement: function() {
5048 ChronoComponent
.prototype.removeElement
.apply(this, arguments
);
5050 this.clearDragListeners();
5055 ------------------------------------------------------------------------------------------------------------------*/
5058 // Binds DOM handlers to elements that reside outside the grid, such as the document
5059 bindGlobalHandlers: function() {
5060 ChronoComponent
.prototype.bindGlobalHandlers
.apply(this, arguments
);
5062 this.listenTo($(document
), {
5063 dragstart
: this.externalDragStart
, // jqui
5064 sortstart
: this.externalDragStart
// jqui
5069 // Unbinds DOM handlers from elements that reside outside the grid
5070 unbindGlobalHandlers: function() {
5071 ChronoComponent
.prototype.unbindGlobalHandlers
.apply(this, arguments
);
5073 this.stopListeningTo($(document
));
5077 // Process a mousedown on an element that represents a day. For day clicking and selecting.
5078 dayMousedown: function(ev
) {
5081 // This will still work even though bindDayHandler doesn't use GlobalEmitter.
5082 if (GlobalEmitter
.get().shouldIgnoreMouse()) {
5086 this.dayClickListener
.startInteraction(ev
);
5088 if (this.opt('selectable')) {
5089 this.daySelectListener
.startInteraction(ev
, {
5090 distance
: this.opt('selectMinDistance')
5096 dayTouchStart: function(ev
) {
5097 var view
= this.view
;
5098 var selectLongPressDelay
;
5100 // On iOS (and Android?) when a new selection is initiated overtop another selection,
5101 // the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
5102 // HACK: simply don't allow this to happen.
5103 // ALSO: prevent selection when an *event* is already raised.
5104 if (view
.isSelected
|| view
.selectedEvent
) {
5108 selectLongPressDelay
= this.opt('selectLongPressDelay');
5109 if (selectLongPressDelay
== null) {
5110 selectLongPressDelay
= this.opt('longPressDelay'); // fallback
5113 this.dayClickListener
.startInteraction(ev
);
5115 if (this.opt('selectable')) {
5116 this.daySelectListener
.startInteraction(ev
, {
5117 delay
: selectLongPressDelay
5123 // Kills all in-progress dragging.
5124 // Useful for when public API methods that result in re-rendering are invoked during a drag.
5125 // Also useful for when touch devices misbehave and don't fire their touchend.
5126 clearDragListeners: function() {
5127 this.dayClickListener
.endInteraction();
5128 this.daySelectListener
.endInteraction();
5130 if (this.segDragListener
) {
5131 this.segDragListener
.endInteraction(); // will clear this.segDragListener
5133 if (this.segResizeListener
) {
5134 this.segResizeListener
.endInteraction(); // will clear this.segResizeListener
5136 if (this.externalDragListener
) {
5137 this.externalDragListener
.endInteraction(); // will clear this.externalDragListener
5143 ------------------------------------------------------------------------------------------------------------------*/
5146 // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
5147 renderHighlight: function(componentFootprint
) {
5148 this.renderFill('highlight', this.componentFootprintToSegs(componentFootprint
));
5152 // Unrenders the emphasis on a date range
5153 unrenderHighlight: function() {
5154 this.unrenderFill('highlight');
5158 /* Converting eventRange -> eventFootprint
5159 ------------------------------------------------------------------------------------------------------------------*/
5162 eventRangesToEventFootprints: function(eventRanges
) {
5163 var eventFootprints
= [];
5166 for (i
= 0; i
< eventRanges
.length
; i
++) {
5167 eventFootprints
.push
.apply(eventFootprints
,
5168 this.eventRangeToEventFootprints(eventRanges
[i
])
5172 return eventFootprints
;
5176 // Given an event's unzoned date range, return an array of eventSpan objects.
5177 // eventSpan - { start, end, isStart, isEnd, otherthings... }
5178 // Subclasses can override.
5179 // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
5180 // TODO: somehow more DRY with Calendar::eventRangeToEventFootprints
5181 eventRangeToEventFootprints: function(eventRange
) {
5184 new ComponentFootprint(
5185 eventRange
.unzonedRange
,
5186 eventRange
.eventDef
.isAllDay()
5188 eventRange
.eventDef
,
5189 eventRange
.eventInstance
// might not exist
5195 /* Converting componentFootprint/eventFootprint -> segs
5196 ------------------------------------------------------------------------------------------------------------------*/
5199 eventFootprintsToSegs: function(eventFootprints
) {
5203 for (i
= 0; i
< eventFootprints
.length
; i
++) {
5204 segs
.push
.apply(segs
,
5205 this.eventFootprintToSegs(eventFootprints
[i
])
5213 // Given an event's span (unzoned start/end and other misc data), and the event itself,
5214 // slices into segments and attaches event-derived properties to them.
5215 // eventSpan - { start, end, isStart, isEnd, otherthings... }
5216 // constraintRange allow additional clipping. optional. eventually remove this.
5217 eventFootprintToSegs: function(eventFootprint
, constraintRange
) {
5218 var unzonedRange
= eventFootprint
.componentFootprint
.unzonedRange
;
5222 if (constraintRange
) {
5223 unzonedRange
= unzonedRange
.intersect(constraintRange
);
5226 segs
= this.componentFootprintToSegs(eventFootprint
.componentFootprint
);
5228 for (i
= 0; i
< segs
.length
; i
++) {
5231 if (!unzonedRange
.isStart
) {
5232 seg
.isStart
= false;
5234 if (!unzonedRange
.isEnd
) {
5238 seg
.footprint
= eventFootprint
;
5239 // TODO: rename to seg.eventFootprint
5246 componentFootprintToSegs: function(componentFootprint
) {
5247 // subclasses must implement
5256 // Creates a listener that tracks the user's drag across day elements, for day clicking.
5257 buildDayClickListener: function() {
5259 var dayClickHit
; // null if invalid dayClick
5261 var dragListener
= new HitDragListener(this, {
5262 scroll
: this.opt('dragScroll'),
5263 interactionStart: function() {
5264 dayClickHit
= dragListener
.origHit
;
5266 hitOver: function(hit
, isOrig
, origHit
) {
5267 // if user dragged to another cell at any point, it can no longer be a dayClick
5272 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
5275 interactionEnd: function(ev
, isCancelled
) {
5276 var componentFootprint
;
5278 if (!isCancelled
&& dayClickHit
) {
5279 componentFootprint
= _this
.getSafeHitFootprint(dayClickHit
);
5281 if (componentFootprint
) {
5282 _this
.view
.triggerDayClick(componentFootprint
, _this
.getHitEl(dayClickHit
), ev
);
5288 // because dayClickListener won't be called with any time delay, "dragging" will begin immediately,
5289 // which will kill any touchmoving/scrolling. Prevent this.
5290 dragListener
.shouldCancelTouchScroll
= false;
5292 dragListener
.scrollAlwaysKills
= true;
5294 return dragListener
;
5303 // Creates a listener that tracks the user's drag across day elements, for day selecting.
5304 buildDaySelectListener: function() {
5306 var selectionFootprint
; // null if invalid selection
5308 var dragListener
= new HitDragListener(this, {
5309 scroll
: this.opt('dragScroll'),
5310 interactionStart: function() {
5311 selectionFootprint
= null;
5313 dragStart: function() {
5314 _this
.view
.unselect(); // since we could be rendering a new selection, we want to clear any old one
5316 hitOver: function(hit
, isOrig
, origHit
) {
5317 var origHitFootprint
;
5320 if (origHit
) { // click needs to have started on a hit
5322 origHitFootprint
= _this
.getSafeHitFootprint(origHit
);
5323 hitFootprint
= _this
.getSafeHitFootprint(hit
);
5325 if (origHitFootprint
&& hitFootprint
) {
5326 selectionFootprint
= _this
.computeSelection(origHitFootprint
, hitFootprint
);
5329 selectionFootprint
= null;
5332 if (selectionFootprint
) {
5333 _this
.renderSelectionFootprint(selectionFootprint
);
5335 else if (selectionFootprint
=== false) {
5340 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
5341 selectionFootprint
= null;
5342 _this
.unrenderSelection();
5344 hitDone: function() { // called after a hitOut OR before a dragEnd
5347 interactionEnd: function(ev
, isCancelled
) {
5348 if (!isCancelled
&& selectionFootprint
) {
5349 // the selection will already have been rendered. just report it
5350 _this
.view
.reportSelection(selectionFootprint
, ev
);
5355 return dragListener
;
5359 // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
5360 // Given a span (unzoned start/end and other misc data)
5361 renderSelectionFootprint: function(componentFootprint
) {
5362 this.renderHighlight(componentFootprint
);
5366 // Unrenders any visual indications of a selection. Will unrender a highlight by default.
5367 unrenderSelection: function() {
5368 this.unrenderHighlight();
5372 // Given the first and last date-spans of a selection, returns another date-span object.
5373 // Subclasses can override and provide additional data in the span object. Will be passed to renderSelectionFootprint().
5374 // Will return false if the selection is invalid and this should be indicated to the user.
5375 // Will return null/undefined if a selection invalid but no error should be reported.
5376 computeSelection: function(footprint0
, footprint1
) {
5377 var wholeFootprint
= this.computeSelectionFootprint(footprint0
, footprint1
);
5379 if (wholeFootprint
&& !this.isSelectionFootprintAllowed(wholeFootprint
)) {
5383 return wholeFootprint
;
5387 // Given two spans, must return the combination of the two.
5388 // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
5389 // Assumes both footprints are non-open-ended.
5390 computeSelectionFootprint: function(footprint0
, footprint1
) {
5392 footprint0
.unzonedRange
.startMs
,
5393 footprint0
.unzonedRange
.endMs
,
5394 footprint1
.unzonedRange
.startMs
,
5395 footprint1
.unzonedRange
.endMs
5398 ms
.sort(compareNumbers
);
5400 return new ComponentFootprint(
5401 new UnzonedRange(ms
[0], ms
[3]),
5407 isSelectionFootprintAllowed: function(componentFootprint
) {
5408 return this.view
.validUnzonedRange
.containsRange(componentFootprint
.unzonedRange
) &&
5409 this.view
.calendar
.isSelectionFootprintAllowed(componentFootprint
);
5418 // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
5419 // Called by fillSegHtml.
5420 businessHoursSegClasses: function(seg
) {
5421 return [ 'fc-nonbusiness', 'fc-bgevent' ];
5425 // Compute business hour segs for the grid's current date range.
5426 // Caller must ask if whole-day business hours are needed.
5427 buildBusinessHourSegs: function(wholeDay
) {
5428 return this.eventFootprintsToSegs(
5429 this.buildBusinessHourEventFootprints(wholeDay
)
5434 // Compute business hour *events* for the grid's current date range.
5435 // Caller must ask if whole-day business hours are needed.
5437 buildBusinessHourEventFootprints: function(wholeDay
) {
5438 var calendar
= this.view
.calendar
;
5440 return this._buildBusinessHourEventFootprints(wholeDay
, calendar
.opt('businessHours'));
5444 _buildBusinessHourEventFootprints: function(wholeDay
, businessHourDef
) {
5445 var calendar
= this.view
.calendar
;
5446 var eventInstanceGroup
;
5449 eventInstanceGroup
= calendar
.buildBusinessInstanceGroup(
5455 if (eventInstanceGroup
) {
5456 eventRanges
= eventInstanceGroup
.sliceRenderRanges(
5465 return this.eventRangesToEventFootprints(eventRanges
);
5474 segs
: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
5476 // derived from options
5477 // TODO: move initialization from Grid.js
5478 eventTimeFormat
: null,
5479 displayEventTime
: null,
5480 displayEventEnd
: null,
5483 // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
5484 computeEventTimeFormat: function() {
5485 return this.opt('smallTimeFormat');
5489 // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
5490 // Only applies to non-all-day events.
5491 computeDisplayEventTime: function() {
5496 // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
5497 computeDisplayEventEnd: function() {
5502 renderEventsPayload: function(eventsPayload
) {
5503 var id
, eventInstanceGroup
;
5504 var eventRenderRanges
;
5505 var eventFootprints
;
5510 for (id
in eventsPayload
) {
5511 eventInstanceGroup
= eventsPayload
[id
];
5513 eventRenderRanges
= eventInstanceGroup
.sliceRenderRanges(this.view
.activeUnzonedRange
);
5514 eventFootprints
= this.eventRangesToEventFootprints(eventRenderRanges
);
5515 eventSegs
= this.eventFootprintsToSegs(eventFootprints
);
5517 if (eventInstanceGroup
.getEventDef().hasBgRendering()) {
5518 bgSegs
.push
.apply(bgSegs
, // append
5523 fgSegs
.push
.apply(fgSegs
, // append
5529 this.segs
= [].concat( // record all segs
5530 this.renderBgSegs(bgSegs
) || bgSegs
,
5531 this.renderFgSegs(fgSegs
) || fgSegs
5536 // Unrenders all events currently rendered on the grid
5537 unrenderEvents: function() {
5538 this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
5539 this.clearDragListeners();
5541 this.unrenderFgSegs();
5542 this.unrenderBgSegs();
5548 // Retrieves all rendered segment objects currently rendered on the grid
5549 getEventSegs: function() {
5550 return this.segs
|| [];
5554 // Background Segment Rendering
5555 // ---------------------------------------------------------------------------------------------------------------
5556 // TODO: move this to ChronoComponent, but without fill
5559 // Renders the given background event segments onto the grid.
5560 // Returns a subset of the segs that were actually rendered.
5561 renderBgSegs: function(segs
) {
5562 return this.renderFill('bgEvent', segs
);
5566 // Unrenders all the currently rendered background event segments
5567 unrenderBgSegs: function() {
5568 this.unrenderFill('bgEvent');
5572 // Renders a background event element, given the default rendering. Called by the fill system.
5573 bgEventSegEl: function(seg
, el
) {
5574 return this.filterEventRenderEl(seg
.footprint
, el
);
5578 // Generates an array of classNames to be used for the default rendering of a background event.
5579 // Called by fillSegHtml.
5580 bgEventSegClasses: function(seg
) {
5581 var eventDef
= seg
.footprint
.eventDef
;
5583 return [ 'fc-bgevent' ].concat(
5585 eventDef
.source
.className
5590 // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
5591 // Called by fillSegHtml.
5592 bgEventSegCss: function(seg
) {
5594 'background-color': this.getSegSkinCss(seg
)['background-color']
5600 ------------------------------------------------------------------------------------------------------------------*/
5603 // Compute the text that should be displayed on an event's element.
5604 // `range` can be the Event object itself, or something range-like, with at least a `start`.
5605 // If event times are disabled, or the event has no time, will return a blank string.
5606 // If not specified, formatStr will default to the eventTimeFormat setting,
5607 // and displayEnd will default to the displayEventEnd setting.
5608 getEventTimeText: function(eventFootprint
, formatStr
, displayEnd
) {
5609 return this._getEventTimeText(
5610 eventFootprint
.eventInstance
.dateProfile
.start
,
5611 eventFootprint
.eventInstance
.dateProfile
.end
,
5612 eventFootprint
.componentFootprint
.isAllDay
,
5619 _getEventTimeText: function(start
, end
, isAllDay
, formatStr
, displayEnd
) {
5621 if (formatStr
== null) {
5622 formatStr
= this.eventTimeFormat
;
5625 if (displayEnd
== null) {
5626 displayEnd
= this.displayEventEnd
;
5629 if (this.displayEventTime
&& !isAllDay
) {
5630 if (displayEnd
&& end
) {
5631 return this.view
.formatRange(
5632 { start
: start
, end
: end
},
5638 return start
.format(formatStr
);
5646 // Generic utility for generating the HTML classNames for an event segment's element
5647 getSegClasses: function(seg
, isDraggable
, isResizable
) {
5648 var view
= this.view
;
5651 seg
.isStart
? 'fc-start' : 'fc-not-start',
5652 seg
.isEnd
? 'fc-end' : 'fc-not-end'
5653 ].concat(this.getSegCustomClasses(seg
));
5656 classes
.push('fc-draggable');
5659 classes
.push('fc-resizable');
5662 // event is currently selected? attach a className.
5663 if (view
.isEventDefSelected(seg
.footprint
.eventDef
)) {
5664 classes
.push('fc-selected');
5671 // List of classes that were defined by the caller of the API in some way
5672 getSegCustomClasses: function(seg
) {
5673 var eventDef
= seg
.footprint
.eventDef
;
5676 eventDef
.className
, // guaranteed to be an array
5677 eventDef
.source
.className
5682 // Utility for generating event skin-related CSS properties
5683 getSegSkinCss: function(seg
) {
5685 'background-color': this.getSegBackgroundColor(seg
),
5686 'border-color': this.getSegBorderColor(seg
),
5687 color
: this.getSegTextColor(seg
)
5692 // Queries for caller-specified color, then falls back to default
5693 getSegBackgroundColor: function(seg
) {
5694 var eventDef
= seg
.footprint
.eventDef
;
5696 return eventDef
.backgroundColor
||
5698 this.getSegDefaultBackgroundColor(seg
);
5702 getSegDefaultBackgroundColor: function(seg
) {
5703 var source
= seg
.footprint
.eventDef
.source
;
5705 return source
.backgroundColor
||
5707 this.opt('eventBackgroundColor') ||
5708 this.opt('eventColor');
5712 // Queries for caller-specified color, then falls back to default
5713 getSegBorderColor: function(seg
) {
5714 var eventDef
= seg
.footprint
.eventDef
;
5716 return eventDef
.borderColor
||
5718 this.getSegDefaultBorderColor(seg
);
5722 getSegDefaultBorderColor: function(seg
) {
5723 var source
= seg
.footprint
.eventDef
.source
;
5725 return source
.borderColor
||
5727 this.opt('eventBorderColor') ||
5728 this.opt('eventColor');
5732 // Queries for caller-specified color, then falls back to default
5733 getSegTextColor: function(seg
) {
5734 var eventDef
= seg
.footprint
.eventDef
;
5736 return eventDef
.textColor
||
5737 this.getSegDefaultTextColor(seg
);
5741 getSegDefaultTextColor: function(seg
) {
5742 var source
= seg
.footprint
.eventDef
.source
;
5744 return source
.textColor
||
5745 this.opt('eventTextColor');
5749 sortEventSegs: function(segs
) {
5750 segs
.sort(proxy(this, 'compareEventSegs'));
5754 // A cmp function for determining which segments should take visual priority
5755 compareEventSegs: function(seg1
, seg2
) {
5756 var f1
= seg1
.footprint
.componentFootprint
;
5757 var r1
= f1
.unzonedRange
;
5758 var f2
= seg2
.footprint
.componentFootprint
;
5759 var r2
= f2
.unzonedRange
;
5761 return r1
.startMs
- r2
.startMs
|| // earlier events go first
5762 (r2
.endMs
- r2
.startMs
) - (r1
.endMs
- r1
.startMs
) || // tie? longer events go first
5763 f2
.isAllDay
- f1
.isAllDay
|| // tie? put all-day events first (booleans cast to 0/1)
5764 compareByFieldSpecs(
5765 seg1
.footprint
.eventDef
,
5766 seg2
.footprint
.eventDef
,
5767 this.view
.eventOrderSpecs
5777 - event clicking/mouseover/mouseout
5778 - things that are common to event dragging AND resizing
5779 - event helper rendering
5783 // self-config, overridable by subclasses
5784 segSelector
: '.fc-event-container > *', // what constitutes an event element?
5786 mousedOverSeg
: null, // the segment object the user's mouse is over. null if over nothing
5788 // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
5789 // of the date areas. if not defined, assumes to be day and time granularity.
5790 // TODO: port isTimeScale into same system?
5794 // Diffs the two dates, returning a duration, based on granularity of the grid
5795 // TODO: port isTimeScale into this system?
5796 diffDates: function(a
, b
) {
5797 if (this.largeUnit
) {
5798 return diffByUnit(a
, b
, this.largeUnit
);
5801 return diffDayTime(a
, b
);
5806 // Attaches event-element-related handlers for *all* rendered event segments of the view.
5807 bindSegHandlers: function() {
5808 this.bindSegHandlersToEl(this.el
);
5812 // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
5813 bindSegHandlersToEl: function(el
) {
5814 this.bindSegHandlerToEl(el
, 'touchstart', this.handleSegTouchStart
);
5815 this.bindSegHandlerToEl(el
, 'mouseenter', this.handleSegMouseover
);
5816 this.bindSegHandlerToEl(el
, 'mouseleave', this.handleSegMouseout
);
5817 this.bindSegHandlerToEl(el
, 'mousedown', this.handleSegMousedown
);
5818 this.bindSegHandlerToEl(el
, 'click', this.handleSegClick
);
5822 // Executes a handler for any a user-interaction on a segment.
5823 // Handler gets called with (seg, ev), and with the `this` context of the Grid
5824 bindSegHandlerToEl: function(el
, name
, handler
) {
5827 el
.on(name
, this.segSelector
, function(ev
) {
5828 var seg
= $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
5830 // only call the handlers if there is not a drag/resize in progress
5831 if (seg
&& !_this
.isDraggingSeg
&& !_this
.isResizingSeg
) {
5832 return handler
.call(_this
, seg
, ev
); // context will be the Grid
5838 handleSegClick: function(seg
, ev
) {
5839 var res
= this.publiclyTrigger('eventClick', { // can return `false` to cancel
5841 args
: [ seg
.footprint
.getEventLegacy(), ev
, this.view
]
5844 if (res
=== false) {
5845 ev
.preventDefault();
5850 // Updates internal state and triggers handlers for when an event element is moused over
5851 handleSegMouseover: function(seg
, ev
) {
5853 !GlobalEmitter
.get().shouldIgnoreMouse() &&
5856 this.mousedOverSeg
= seg
;
5858 if (this.view
.isEventDefResizable(seg
.footprint
.eventDef
)) {
5859 seg
.el
.addClass('fc-allow-mouse-resize');
5862 this.publiclyTrigger('eventMouseover', {
5864 args
: [ seg
.footprint
.getEventLegacy(), ev
, this.view
]
5870 // Updates internal state and triggers handlers for when an event element is moused out.
5871 // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
5872 handleSegMouseout: function(seg
, ev
) {
5873 ev
= ev
|| {}; // if given no args, make a mock mouse event
5875 if (this.mousedOverSeg
) {
5876 seg
= seg
|| this.mousedOverSeg
; // if given no args, use the currently moused-over segment
5877 this.mousedOverSeg
= null;
5879 if (this.view
.isEventDefResizable(seg
.footprint
.eventDef
)) {
5880 seg
.el
.removeClass('fc-allow-mouse-resize');
5883 this.publiclyTrigger('eventMouseout', {
5885 args
: [ seg
.footprint
.getEventLegacy(), ev
, this.view
]
5891 handleSegMousedown: function(seg
, ev
) {
5892 var isResizing
= this.startSegResize(seg
, ev
, { distance
: 5 });
5894 if (!isResizing
&& this.view
.isEventDefDraggable(seg
.footprint
.eventDef
)) {
5895 this.buildSegDragListener(seg
)
5896 .startInteraction(ev
, {
5903 handleSegTouchStart: function(seg
, ev
) {
5904 var view
= this.view
;
5905 var eventDef
= seg
.footprint
.eventDef
;
5906 var isSelected
= view
.isEventDefSelected(eventDef
);
5907 var isDraggable
= view
.isEventDefDraggable(eventDef
);
5908 var isResizable
= view
.isEventDefResizable(eventDef
);
5909 var isResizing
= false;
5911 var eventLongPressDelay
;
5913 if (isSelected
&& isResizable
) {
5914 // only allow resizing of the event is selected
5915 isResizing
= this.startSegResize(seg
, ev
);
5918 if (!isResizing
&& (isDraggable
|| isResizable
)) { // allowed to be selected?
5920 eventLongPressDelay
= this.opt('eventLongPressDelay');
5921 if (eventLongPressDelay
== null) {
5922 eventLongPressDelay
= this.opt('longPressDelay'); // fallback
5925 dragListener
= isDraggable
?
5926 this.buildSegDragListener(seg
) :
5927 this.buildSegSelectListener(seg
); // seg isn't draggable, but still needs to be selected
5929 dragListener
.startInteraction(ev
, { // won't start if already started
5930 delay
: isSelected
? 0 : eventLongPressDelay
// do delay if not already selected
5936 // seg isn't draggable, but let's use a generic DragListener
5937 // simply for the delay, so it can be selected.
5938 // Has side effect of setting/unsetting `segDragListener`
5939 buildSegSelectListener: function(seg
) {
5941 var view
= this.view
;
5942 var eventDef
= seg
.footprint
.eventDef
;
5943 var eventInstance
= seg
.footprint
.eventInstance
; // null for inverse-background events
5945 if (this.segDragListener
) {
5946 return this.segDragListener
;
5949 var dragListener
= this.segDragListener
= new DragListener({
5950 dragStart: function(ev
) {
5952 dragListener
.isTouch
&&
5953 !view
.isEventDefSelected(eventDef
) &&
5956 // if not previously selected, will fire after a delay. then, select the event
5957 view
.selectEventInstance(eventInstance
);
5960 interactionEnd: function(ev
) {
5961 _this
.segDragListener
= null;
5965 return dragListener
;
5969 // is it allowed, in relation to the view's validRange?
5970 // NOTE: very similar to isExternalInstanceGroupAllowed
5971 isEventInstanceGroupAllowed: function(eventInstanceGroup
) {
5972 var eventFootprints
= this.eventRangesToEventFootprints(eventInstanceGroup
.getAllEventRanges());
5975 for (i
= 0; i
< eventFootprints
.length
; i
++) {
5976 // TODO: just use getAllEventRanges directly
5977 if (!this.view
.validUnzonedRange
.containsRange(eventFootprints
[i
].componentFootprint
.unzonedRange
)) {
5982 return this.view
.calendar
.isEventInstanceGroupAllowed(eventInstanceGroup
);
5987 ------------------------------------------------------------------------------------------------------------------*/
5988 // TODO: should probably move this to Grid.events, like we did event dragging / resizing
5991 renderHelperEventFootprints: function(eventFootprints
, sourceSeg
) {
5992 return this.renderHelperEventFootprintEls(eventFootprints
, sourceSeg
)
5993 .addClass('fc-helper');
5997 renderHelperEventFootprintEls: function(eventFootprints
, sourceSeg
) {
5998 // Subclasses must implement.
5999 // Must return all mock event elements.
6003 // Unrenders a mock event
6004 // TODO: have this in ChronoComponent
6005 unrenderHelper: function() {
6006 // subclasses must implement
6010 fabricateEventFootprint: function(componentFootprint
) {
6011 var calendar
= this.view
.calendar
;
6012 var eventDateProfile
= calendar
.footprintToDateProfile(componentFootprint
);
6013 var dummyEvent
= new SingleEventDef(new EventSource(calendar
));
6016 dummyEvent
.dateProfile
= eventDateProfile
;
6017 dummyInstance
= dummyEvent
.buildInstance();
6019 return new EventFootprint(componentFootprint
, dummyEvent
, dummyInstance
);
6027 Wired up via Grid.event-interation.js by calling
6028 buildSegDragListener
6032 isDraggingSeg
: false, // is a segment being dragged? boolean
6035 // Builds a listener that will track user-dragging on an event segment.
6036 // Generic enough to work with any type of Grid.
6037 // Has side effect of setting/unsetting `segDragListener`
6038 buildSegDragListener: function(seg
) {
6040 var view
= this.view
;
6041 var calendar
= view
.calendar
;
6042 var eventManager
= calendar
.eventManager
;
6044 var eventDef
= seg
.footprint
.eventDef
;
6045 var eventInstance
= seg
.footprint
.eventInstance
; // null for inverse-background events
6047 var mouseFollower
; // A clone of the original element that will move with the mouse
6048 var eventDefMutation
;
6050 if (this.segDragListener
) {
6051 return this.segDragListener
;
6054 // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
6056 var dragListener
= this.segDragListener
= new HitDragListener(view
, {
6057 scroll
: this.opt('dragScroll'),
6059 subjectCenter
: true,
6060 interactionStart: function(ev
) {
6061 seg
.component
= _this
; // for renderDrag
6063 mouseFollower
= new MouseFollower(seg
.el
, {
6064 additionalClass
: 'fc-dragging',
6066 opacity
: dragListener
.isTouch
? null : _this
.opt('dragOpacity'),
6067 revertDuration
: _this
.opt('dragRevertDuration'),
6068 zIndex
: 2 // one above the .fc-view
6070 mouseFollower
.hide(); // don't show until we know this is a real drag
6071 mouseFollower
.start(ev
);
6073 dragStart: function(ev
) {
6075 dragListener
.isTouch
&&
6076 !view
.isEventDefSelected(eventDef
) &&
6079 // if not previously selected, will fire after a delay. then, select the event
6080 view
.selectEventInstance(eventInstance
);
6083 _this
.handleSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
6084 _this
.segDragStart(seg
, ev
);
6085 view
.hideEventsWithId(eventDef
.id
); // hide all event segments. our mouseFollower will take over
6087 hitOver: function(hit
, isOrig
, origHit
) {
6088 var isAllowed
= true;
6091 var mutatedEventInstanceGroup
;
6094 // starting hit could be forced (DayGrid.limit)
6099 // hit might not belong to this grid, so query origin grid
6100 origFootprint
= origHit
.component
.getSafeHitFootprint(origHit
);
6101 footprint
= hit
.component
.getSafeHitFootprint(hit
);
6103 if (origFootprint
&& footprint
) {
6104 eventDefMutation
= _this
.computeEventDropMutation(origFootprint
, footprint
, eventDef
);
6106 if (eventDefMutation
) {
6107 mutatedEventInstanceGroup
= eventManager
.buildMutatedEventInstanceGroup(
6111 isAllowed
= _this
.isEventInstanceGroupAllowed(mutatedEventInstanceGroup
);
6122 eventDefMutation
= null;
6126 // if a valid drop location, have the subclass render a visual indication
6129 (dragHelperEls
= view
.renderDrag(
6130 _this
.eventRangesToEventFootprints(
6131 mutatedEventInstanceGroup
.sliceRenderRanges(_this
.unzonedRange
, calendar
)
6136 dragHelperEls
.addClass('fc-dragging');
6137 if (!dragListener
.isTouch
) {
6138 _this
.applyDragOpacity(dragHelperEls
);
6141 mouseFollower
.hide(); // if the subclass is already using a mock event "helper", hide our own
6144 mouseFollower
.show(); // otherwise, have the helper follow the mouse (no snapping)
6148 // needs to have moved hits to be a valid drop
6149 eventDefMutation
= null;
6152 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
6153 view
.unrenderDrag(); // unrender whatever was done in renderDrag
6154 mouseFollower
.show(); // show in case we are moving out of all hits
6155 eventDefMutation
= null;
6157 hitDone: function() { // Called after a hitOut OR before a dragEnd
6160 interactionEnd: function(ev
) {
6161 delete seg
.component
; // prevent side effects
6163 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
6164 mouseFollower
.stop(!eventDefMutation
, function() {
6166 view
.unrenderDrag();
6167 _this
.segDragStop(seg
, ev
);
6170 if (eventDefMutation
) {
6171 // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
6172 view
.reportEventDrop(eventInstance
, eventDefMutation
, el
, ev
);
6175 view
.showEventsWithId(eventDef
.id
);
6178 _this
.segDragListener
= null;
6182 return dragListener
;
6186 // Called before event segment dragging starts
6187 segDragStart: function(seg
, ev
) {
6188 this.isDraggingSeg
= true;
6189 this.publiclyTrigger('eventDragStart', {
6192 seg
.footprint
.getEventLegacy(),
6201 // Called after event segment dragging stops
6202 segDragStop: function(seg
, ev
) {
6203 this.isDraggingSeg
= false;
6204 this.publiclyTrigger('eventDragStop', {
6207 seg
.footprint
.getEventLegacy(),
6216 // DOES NOT consider overlap/constraint
6217 computeEventDropMutation: function(startFootprint
, endFootprint
, eventDef
) {
6218 var date0
= startFootprint
.unzonedRange
.getStart();
6219 var date1
= endFootprint
.unzonedRange
.getStart();
6220 var clearEnd
= false;
6221 var forceTimed
= false;
6222 var forceAllDay
= false;
6225 var eventDefMutation
;
6227 if (startFootprint
.isAllDay
!== endFootprint
.isAllDay
) {
6230 if (endFootprint
.isAllDay
) {
6239 dateDelta
= this.diffDates(date1
, date0
);
6241 dateMutation
= new EventDefDateMutation();
6242 dateMutation
.clearEnd
= clearEnd
;
6243 dateMutation
.forceTimed
= forceTimed
;
6244 dateMutation
.forceAllDay
= forceAllDay
;
6245 dateMutation
.setDateDelta(dateDelta
);
6247 eventDefMutation
= new EventDefMutation();
6248 eventDefMutation
.setDateMutation(dateMutation
);
6250 return eventDefMutation
;
6254 // Utility for apply dragOpacity to a jQuery set
6255 applyDragOpacity: function(els
) {
6256 var opacity
= this.opt('dragOpacity');
6258 if (opacity
!= null) {
6259 els
.css('opacity', opacity
);
6268 Wired up via Grid.event-interation.js by calling
6273 isResizingSeg
: false, // is a segment being resized? boolean
6276 // returns boolean whether resizing actually started or not.
6277 // assumes the seg allows resizing.
6278 // `dragOptions` are optional.
6279 startSegResize: function(seg
, ev
, dragOptions
) {
6280 if ($(ev
.target
).is('.fc-resizer')) {
6281 this.buildSegResizeListener(seg
, $(ev
.target
).is('.fc-start-resizer'))
6282 .startInteraction(ev
, dragOptions
);
6289 // Creates a listener that tracks the user as they resize an event segment.
6290 // Generic enough to work with any type of Grid.
6291 buildSegResizeListener: function(seg
, isStart
) {
6293 var view
= this.view
;
6294 var calendar
= view
.calendar
;
6295 var eventManager
= calendar
.eventManager
;
6297 var eventDef
= seg
.footprint
.eventDef
;
6298 var eventInstance
= seg
.footprint
.eventInstance
;
6300 var resizeMutation
; // zoned event date properties. falsy if invalid resize
6302 // Tracks mouse movement over the *grid's* coordinate map
6303 var dragListener
= this.segResizeListener
= new HitDragListener(this, {
6304 scroll
: this.opt('dragScroll'),
6306 interactionStart: function() {
6309 dragStart: function(ev
) {
6311 _this
.handleSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
6312 _this
.segResizeStart(seg
, ev
);
6314 hitOver: function(hit
, isOrig
, origHit
) {
6315 var isAllowed
= true;
6316 var origHitFootprint
= _this
.getSafeHitFootprint(origHit
);
6317 var hitFootprint
= _this
.getSafeHitFootprint(hit
);
6318 var mutatedEventInstanceGroup
;
6320 if (origHitFootprint
&& hitFootprint
) {
6321 resizeMutation
= isStart
?
6322 _this
.computeEventStartResizeMutation(origHitFootprint
, hitFootprint
, seg
.footprint
) :
6323 _this
.computeEventEndResizeMutation(origHitFootprint
, hitFootprint
, seg
.footprint
);
6325 if (resizeMutation
) {
6326 mutatedEventInstanceGroup
= eventManager
.buildMutatedEventInstanceGroup(
6330 isAllowed
= _this
.isEventInstanceGroupAllowed(mutatedEventInstanceGroup
);
6341 resizeMutation
= null;
6344 else if (resizeMutation
.isEmpty()) {
6345 // no change. (FYI, event dates might have zones)
6346 resizeMutation
= null;
6349 if (resizeMutation
) {
6350 view
.hideEventsWithId(eventDef
.id
);
6352 _this
.renderEventResize(
6353 _this
.eventRangesToEventFootprints(
6354 mutatedEventInstanceGroup
.sliceRenderRanges(_this
.unzonedRange
, calendar
)
6360 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
6361 resizeMutation
= null;
6362 view
.showEventsWithId(eventDef
.id
); // for when out-of-bounds. show original
6364 hitDone: function() { // resets the rendering to show the original event
6365 _this
.unrenderEventResize();
6368 interactionEnd: function(ev
) {
6370 _this
.segResizeStop(seg
, ev
);
6373 if (resizeMutation
) { // valid date to resize to?
6374 // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
6375 view
.reportEventResize(eventInstance
, resizeMutation
, el
, ev
);
6378 view
.showEventsWithId(eventDef
.id
);
6380 _this
.segResizeListener
= null;
6384 return dragListener
;
6388 // Called before event segment resizing starts
6389 segResizeStart: function(seg
, ev
) {
6390 this.isResizingSeg
= true;
6391 this.publiclyTrigger('eventResizeStart', {
6394 seg
.footprint
.getEventLegacy(),
6403 // Called after event segment resizing stops
6404 segResizeStop: function(seg
, ev
) {
6405 this.isResizingSeg
= false;
6406 this.publiclyTrigger('eventResizeStop', {
6409 seg
.footprint
.getEventLegacy(),
6418 // Returns new date-information for an event segment being resized from its start
6419 computeEventStartResizeMutation: function(startFootprint
, endFootprint
, origEventFootprint
) {
6420 var origRange
= origEventFootprint
.componentFootprint
.unzonedRange
;
6421 var startDelta
= this.diffDates(
6422 endFootprint
.unzonedRange
.getStart(),
6423 startFootprint
.unzonedRange
.getStart()
6426 var eventDefMutation
;
6428 if (origRange
.getStart().add(startDelta
) < origRange
.getEnd()) {
6430 dateMutation
= new EventDefDateMutation();
6431 dateMutation
.setStartDelta(startDelta
);
6433 eventDefMutation
= new EventDefMutation();
6434 eventDefMutation
.setDateMutation(dateMutation
);
6436 return eventDefMutation
;
6443 // Returns new date-information for an event segment being resized from its end
6444 computeEventEndResizeMutation: function(startFootprint
, endFootprint
, origEventFootprint
) {
6445 var origRange
= origEventFootprint
.componentFootprint
.unzonedRange
;
6446 var endDelta
= this.diffDates(
6447 endFootprint
.unzonedRange
.getEnd(),
6448 startFootprint
.unzonedRange
.getEnd()
6451 var eventDefMutation
;
6453 if (origRange
.getEnd().add(endDelta
) > origRange
.getStart()) {
6455 dateMutation
= new EventDefDateMutation();
6456 dateMutation
.setEndDelta(endDelta
);
6458 eventDefMutation
= new EventDefMutation();
6459 eventDefMutation
.setDateMutation(dateMutation
);
6461 return eventDefMutation
;
6468 // Renders a visual indication of an event being resized.
6469 // Must return elements used for any mock events.
6470 renderEventResize: function(eventFootprints
, seg
) {
6471 // subclasses must implement
6475 // Unrenders a visual indication of an event being resized.
6476 unrenderEventResize: function() {
6477 // subclasses must implement
6485 Wired up via Grid.js by calling
6490 isDraggingExternal
: false, // jqui-dragging an external element? boolean
6493 // Called when a jQuery UI drag is initiated anywhere in the DOM
6494 externalDragStart: function(ev
, ui
) {
6498 if (this.opt('droppable')) { // only listen if this setting is on
6499 el
= $((ui
? ui
.item
: null) || ev
.target
);
6501 // Test that the dragged element passes the dropAccept selector or filter function.
6502 // FYI, the default is "*" (matches all)
6503 accept
= this.opt('dropAccept');
6504 if ($.isFunction(accept
) ? accept
.call(el
[0], el
) : el
.is(accept
)) {
6505 if (!this.isDraggingExternal
) { // prevent double-listening if fired twice
6506 this.listenToExternalDrag(el
, ev
, ui
);
6513 // Called when a jQuery UI drag starts and it needs to be monitored for dropping
6514 listenToExternalDrag: function(el
, ev
, ui
) {
6516 var view
= this.view
;
6517 var meta
= getDraggedElMeta(el
); // extra data about event drop, including possible event to create
6518 var singleEventDef
; // a null value signals an unsuccessful drag
6520 // listener that tracks mouse movement over date-associated pixel regions
6521 var dragListener
= _this
.externalDragListener
= new HitDragListener(this, {
6522 interactionStart: function() {
6523 _this
.isDraggingExternal
= true;
6525 hitOver: function(hit
) {
6526 var isAllowed
= true;
6527 var hitFootprint
= hit
.component
.getSafeHitFootprint(hit
); // hit might not belong to this grid
6528 var mutatedEventInstanceGroup
;
6531 singleEventDef
= _this
.computeExternalDrop(hitFootprint
, meta
);
6533 if (singleEventDef
) {
6534 mutatedEventInstanceGroup
= new EventInstanceGroup(
6535 singleEventDef
.buildInstances()
6537 isAllowed
= meta
.eventProps
? // isEvent?
6538 _this
.isEventInstanceGroupAllowed(mutatedEventInstanceGroup
) :
6539 _this
.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup
);
6550 singleEventDef
= null;
6554 if (singleEventDef
) {
6555 _this
.renderDrag( // called without a seg parameter
6556 _this
.eventRangesToEventFootprints(
6557 mutatedEventInstanceGroup
.sliceRenderRanges(_this
.unzonedRange
, view
.calendar
)
6562 hitOut: function() {
6563 singleEventDef
= null; // signal unsuccessful
6565 hitDone: function() { // Called after a hitOut OR before a dragEnd
6567 _this
.unrenderDrag();
6569 interactionEnd: function(ev
) {
6571 if (singleEventDef
) { // element was dropped on a valid hit
6572 view
.reportExternalDrop(
6574 Boolean(meta
.eventProps
), // isEvent
6575 Boolean(meta
.stick
), // isSticky
6580 _this
.isDraggingExternal
= false;
6581 _this
.externalDragListener
= null;
6585 dragListener
.startDrag(ev
); // start listening immediately
6589 // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
6590 // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
6591 // Returning a null value signals an invalid drop hit.
6592 // DOES NOT consider overlap/constraint.
6593 // Assumes both footprints are non-open-ended.
6594 computeExternalDrop: function(componentFootprint
, meta
) {
6595 var calendar
= this.view
.calendar
;
6596 var start
= FC
.moment
.utc(componentFootprint
.unzonedRange
.startMs
).stripZone();
6600 if (componentFootprint
.isAllDay
) {
6601 // if dropped on an all-day span, and element's metadata specified a time, set it
6602 if (meta
.startTime
) {
6603 start
.time(meta
.startTime
);
6610 if (meta
.duration
) {
6611 end
= start
.clone().add(meta
.duration
);
6614 start
= calendar
.applyTimezone(start
);
6617 end
= calendar
.applyTimezone(end
);
6620 eventDef
= SingleEventDef
.parse(
6621 $.extend({}, meta
.eventProps
, {
6625 new EventSource(calendar
)
6632 // NOTE: very similar to isEventInstanceGroupAllowed
6633 // when it's a completely anonymous external drag, no event.
6634 isExternalInstanceGroupAllowed: function(eventInstanceGroup
) {
6635 var calendar
= this.view
.calendar
;
6636 var eventFootprints
= this.eventRangesToEventFootprints(eventInstanceGroup
.getAllEventRanges());
6639 for (i
= 0; i
< eventFootprints
.length
; i
++) {
6640 if (!this.view
.validUnzonedRange
.containsRange(eventFootprints
[i
].componentFootprint
.unzonedRange
)) {
6645 for (i
= 0; i
< eventFootprints
.length
; i
++) {
6646 // treat it as a selection
6647 // TODO: pass in eventInstanceGroup instead
6648 // because we don't want calendar's constraint system to depend on a component's
6649 // determination of footprints.
6650 if (!calendar
.isSelectionFootprintAllowed(eventFootprints
[i
].componentFootprint
)) {
6661 /* External-Dragging-Element Data
6662 ----------------------------------------------------------------------------------------------------------------------*/
6664 // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
6665 // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
6666 FC
.dataAttrPrefix
= '';
6668 // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
6669 // to be used for Event Object creation.
6670 // A defined `.eventProps`, even when empty, indicates that an event should be created.
6671 function getDraggedElMeta(el
) {
6672 var prefix
= FC
.dataAttrPrefix
;
6673 var eventProps
; // properties for creating the event, not related to date/time
6674 var startTime
; // a Duration
6678 if (prefix
) { prefix
+= '-'; }
6679 eventProps
= el
.data(prefix
+ 'event') || null;
6682 if (typeof eventProps
=== 'object') {
6683 eventProps
= $.extend({}, eventProps
); // make a copy
6685 else { // something like 1 or true. still signal event creation
6689 // pluck special-cased date/time properties
6690 startTime
= eventProps
.start
;
6691 if (startTime
== null) { startTime
= eventProps
.time
; } // accept 'time' as well
6692 duration
= eventProps
.duration
;
6693 stick
= eventProps
.stick
;
6694 delete eventProps
.start
;
6695 delete eventProps
.time
;
6696 delete eventProps
.duration
;
6697 delete eventProps
.stick
;
6700 // fallback to standalone attribute values for each of the date/time properties
6701 if (startTime
== null) { startTime
= el
.data(prefix
+ 'start'); }
6702 if (startTime
== null) { startTime
= el
.data(prefix
+ 'time'); } // accept 'time' as well
6703 if (duration
== null) { duration
= el
.data(prefix
+ 'duration'); }
6704 if (stick
== null) { stick
= el
.data(prefix
+ 'stick'); }
6706 // massage into correct data types
6707 startTime
= startTime
!= null ? moment
.duration(startTime
) : null;
6708 duration
= duration
!= null ? moment
.duration(duration
) : null;
6709 stick
= Boolean(stick
);
6711 return { eventProps
: eventProps
, startTime
: startTime
, duration
: duration
, stick
: stick
};
6718 /* Fill System (highlight, background events, business hours)
6719 --------------------------------------------------------------------------------------------------------------------
6720 TODO: remove this system. like we did in TimeGrid
6724 elsByFill
: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
6727 initFillInternals: function() {
6728 this.elsByFill
= {};
6732 // Renders a set of rectangles over the given segments of time.
6733 // MUST RETURN a subset of segs, the segs that were actually rendered.
6734 // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
6735 renderFill: function(type
, segs
) {
6736 // subclasses must implement
6740 // Unrenders a specific type of fill that is currently rendered on the grid
6741 unrenderFill: function(type
) {
6742 var el
= this.elsByFill
[type
];
6746 delete this.elsByFill
[type
];
6751 // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
6752 // Only returns segments that successfully rendered.
6753 // To be harnessed by renderFill (implemented by subclasses).
6754 // Analagous to renderFgSegEls.
6755 renderFillSegEls: function(type
, segs
) {
6757 var segElMethod
= this[type
+ 'SegEl'];
6759 var renderedSegs
= [];
6764 // build a large concatenation of segment HTML
6765 for (i
= 0; i
< segs
.length
; i
++) {
6766 html
+= this.fillSegHtml(type
, segs
[i
]);
6769 // Grab individual elements from the combined HTML string. Use each as the default rendering.
6770 // Then, compute the 'el' for each segment.
6771 $(html
).each(function(i
, node
) {
6775 // allow custom filter methods per-type
6777 el
= segElMethod
.call(_this
, seg
, el
);
6780 if (el
) { // custom filters did not cancel the render
6781 el
= $(el
); // allow custom filter to return raw DOM node
6783 // correct element type? (would be bad if a non-TD were inserted into a table for example)
6784 if (el
.is(_this
.fillSegTag
)) {
6786 renderedSegs
.push(seg
);
6792 return renderedSegs
;
6796 fillSegTag
: 'div', // subclasses can override
6799 // Builds the HTML needed for one fill segment. Generic enough to work with different types.
6800 fillSegHtml: function(type
, seg
) {
6802 // custom hooks per-type
6803 var classesMethod
= this[type
+ 'SegClasses'];
6804 var cssMethod
= this[type
+ 'SegCss'];
6806 var classes
= classesMethod
? classesMethod
.call(this, seg
) : [];
6807 var css
= cssToStr(cssMethod
? cssMethod
.call(this, seg
) : {});
6809 return '<' + this.fillSegTag
+
6810 (classes
.length
? ' class="' + classes
.join(' ') + '"' : '') +
6811 (css
? ' style="' + css
+ '"' : '') +
6816 // Generates an array of classNames for rendering the highlight. Used by the fill system.
6817 highlightSegClasses: function() {
6818 return [ 'fc-highlight' ];
6826 A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
6827 Prerequisite: the object being mixed into needs to be a *Grid*
6829 var DayTableMixin
= FC
.DayTableMixin
= {
6831 breakOnWeeks
: false, // should create a new row for each week?
6832 dayDates
: null, // whole-day dates for each column. left to right
6833 dayIndices
: null, // for each day from start, the offset
6837 colHeadFormat
: null,
6840 // Populates internal variables used for date calculation and rendering
6841 updateDayTable: function() {
6842 var view
= this.view
;
6843 var calendar
= view
.calendar
;
6844 var date
= calendar
.msToUtcMoment(this.unzonedRange
.startMs
, true);
6845 var end
= calendar
.msToUtcMoment(this.unzonedRange
.endMs
, true);
6847 var dayIndices
= [];
6853 while (date
.isBefore(end
)) { // loop each day from start to end
6854 if (view
.isHiddenDay(date
)) {
6855 dayIndices
.push(dayIndex
+ 0.5); // mark that it's between indices
6859 dayIndices
.push(dayIndex
);
6860 dayDates
.push(date
.clone());
6862 date
.add(1, 'days');
6865 if (this.breakOnWeeks
) {
6866 // count columns until the day-of-week repeats
6867 firstDay
= dayDates
[0].day();
6868 for (daysPerRow
= 1; daysPerRow
< dayDates
.length
; daysPerRow
++) {
6869 if (dayDates
[daysPerRow
].day() == firstDay
) {
6873 rowCnt
= Math
.ceil(dayDates
.length
/ daysPerRow
);
6877 daysPerRow
= dayDates
.length
;
6880 this.dayDates
= dayDates
;
6881 this.dayIndices
= dayIndices
;
6882 this.daysPerRow
= daysPerRow
;
6883 this.rowCnt
= rowCnt
;
6885 this.updateDayTableCols();
6889 // Computes and assigned the colCnt property and updates any options that may be computed from it
6890 updateDayTableCols: function() {
6891 this.colCnt
= this.computeColCnt();
6892 this.colHeadFormat
= this.opt('columnFormat') || this.computeColHeadFormat();
6896 // Determines how many columns there should be in the table
6897 computeColCnt: function() {
6898 return this.daysPerRow
;
6902 // Computes the ambiguously-timed moment for the given cell
6903 getCellDate: function(row
, col
) {
6904 return this.dayDates
[
6905 this.getCellDayIndex(row
, col
)
6910 // Computes the ambiguously-timed date range for the given cell
6911 getCellRange: function(row
, col
) {
6912 var start
= this.getCellDate(row
, col
);
6913 var end
= start
.clone().add(1, 'days');
6915 return { start
: start
, end
: end
};
6919 // Returns the number of day cells, chronologically, from the first of the grid (0-based)
6920 getCellDayIndex: function(row
, col
) {
6921 return row
* this.daysPerRow
+ this.getColDayIndex(col
);
6925 // Returns the numner of day cells, chronologically, from the first cell in *any given row*
6926 getColDayIndex: function(col
) {
6928 return this.colCnt
- 1 - col
;
6936 // Given a date, returns its chronolocial cell-index from the first cell of the grid.
6937 // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
6938 // If before the first offset, returns a negative number.
6939 // If after the last offset, returns an offset past the last cell offset.
6940 // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
6941 getDateDayIndex: function(date
) {
6942 var dayIndices
= this.dayIndices
;
6943 var dayOffset
= date
.diff(this.dayDates
[0], 'days');
6945 if (dayOffset
< 0) {
6946 return dayIndices
[0] - 1;
6948 else if (dayOffset
>= dayIndices
.length
) {
6949 return dayIndices
[dayIndices
.length
- 1] + 1;
6952 return dayIndices
[dayOffset
];
6958 ------------------------------------------------------------------------------------------------------------------*/
6961 // Computes a default column header formatting string if `colFormat` is not explicitly defined
6962 computeColHeadFormat: function() {
6963 // if more than one week row, or if there are a lot of columns with not much space,
6964 // put just the day numbers will be in each cell
6965 if (this.rowCnt
> 1 || this.colCnt
> 10) {
6966 return 'ddd'; // "Sat"
6968 // multiple days, so full single date string WON'T be in title text
6969 else if (this.colCnt
> 1) {
6970 return this.opt('dayOfMonthFormat'); // "Sat 12/10"
6972 // single day, so full single date string will probably be in title text
6974 return 'dddd'; // "Saturday"
6980 ------------------------------------------------------------------------------------------------------------------*/
6983 // Slices up a date range into a segment for every week-row it intersects with
6984 sliceRangeByRow: function(unzonedRange
) {
6985 var daysPerRow
= this.daysPerRow
;
6986 var normalRange
= this.view
.computeDayRange(unzonedRange
); // make whole-day range, considering nextDayThreshold
6987 var rangeFirst
= this.getDateDayIndex(normalRange
.start
); // inclusive first index
6988 var rangeLast
= this.getDateDayIndex(normalRange
.end
.clone().subtract(1, 'days')); // inclusive last index
6991 var rowFirst
, rowLast
; // inclusive day-index range for current row
6992 var segFirst
, segLast
; // inclusive day-index range for segment
6994 for (row
= 0; row
< this.rowCnt
; row
++) {
6995 rowFirst
= row
* daysPerRow
;
6996 rowLast
= rowFirst
+ daysPerRow
- 1;
6998 // intersect segment's offset range with the row's
6999 segFirst
= Math
.max(rangeFirst
, rowFirst
);
7000 segLast
= Math
.min(rangeLast
, rowLast
);
7002 // deal with in-between indices
7003 segFirst
= Math
.ceil(segFirst
); // in-between starts round to next cell
7004 segLast
= Math
.floor(segLast
); // in-between ends round to prev cell
7006 if (segFirst
<= segLast
) { // was there any intersection with the current row?
7010 // normalize to start of row
7011 firstRowDayIndex
: segFirst
- rowFirst
,
7012 lastRowDayIndex
: segLast
- rowFirst
,
7014 // must be matching integers to be the segment's start/end
7015 isStart
: segFirst
=== rangeFirst
,
7016 isEnd
: segLast
=== rangeLast
7025 // Slices up a date range into a segment for every day-cell it intersects with.
7026 // TODO: make more DRY with sliceRangeByRow somehow.
7027 sliceRangeByDay: function(unzonedRange
) {
7028 var daysPerRow
= this.daysPerRow
;
7029 var normalRange
= this.view
.computeDayRange(unzonedRange
); // make whole-day range, considering nextDayThreshold
7030 var rangeFirst
= this.getDateDayIndex(normalRange
.start
); // inclusive first index
7031 var rangeLast
= this.getDateDayIndex(normalRange
.end
.clone().subtract(1, 'days')); // inclusive last index
7034 var rowFirst
, rowLast
; // inclusive day-index range for current row
7036 var segFirst
, segLast
; // inclusive day-index range for segment
7038 for (row
= 0; row
< this.rowCnt
; row
++) {
7039 rowFirst
= row
* daysPerRow
;
7040 rowLast
= rowFirst
+ daysPerRow
- 1;
7042 for (i
= rowFirst
; i
<= rowLast
; i
++) {
7044 // intersect segment's offset range with the row's
7045 segFirst
= Math
.max(rangeFirst
, i
);
7046 segLast
= Math
.min(rangeLast
, i
);
7048 // deal with in-between indices
7049 segFirst
= Math
.ceil(segFirst
); // in-between starts round to next cell
7050 segLast
= Math
.floor(segLast
); // in-between ends round to prev cell
7052 if (segFirst
<= segLast
) { // was there any intersection with the current row?
7056 // normalize to start of row
7057 firstRowDayIndex
: segFirst
- rowFirst
,
7058 lastRowDayIndex
: segLast
- rowFirst
,
7060 // must be matching integers to be the segment's start/end
7061 isStart
: segFirst
=== rangeFirst
,
7062 isEnd
: segLast
=== rangeLast
7073 ------------------------------------------------------------------------------------------------------------------*/
7076 renderHeadHtml: function() {
7077 var theme
= this.view
.calendar
.theme
;
7080 '<div class="fc-row ' + theme
.getClass('headerRow') + '">' +
7081 '<table class="' + theme
.getClass('tableGrid') + '">' +
7083 this.renderHeadTrHtml() +
7090 renderHeadIntroHtml: function() {
7091 return this.renderIntroHtml(); // fall back to generic
7095 renderHeadTrHtml: function() {
7098 (this.isRTL
? '' : this.renderHeadIntroHtml()) +
7099 this.renderHeadDateCellsHtml() +
7100 (this.isRTL
? this.renderHeadIntroHtml() : '') +
7105 renderHeadDateCellsHtml: function() {
7109 for (col
= 0; col
< this.colCnt
; col
++) {
7110 date
= this.getCellDate(0, col
);
7111 htmls
.push(this.renderHeadDateCellHtml(date
));
7114 return htmls
.join('');
7118 // TODO: when internalApiVersion, accept an object for HTML attributes
7119 // (colspan should be no different)
7120 renderHeadDateCellHtml: function(date
, colspan
, otherAttrs
) {
7121 var view
= this.view
;
7122 var isDateValid
= view
.activeUnzonedRange
.containsDate(date
); // TODO: called too frequently. cache somehow.
7125 view
.calendar
.theme
.getClass('widgetHeader')
7127 var innerHtml
= htmlEscape(date
.format(this.colHeadFormat
));
7129 // if only one row of days, the classNames on the header can represent the specific days beneath
7130 if (this.rowCnt
=== 1) {
7131 classNames
= classNames
.concat(
7132 // includes the day-of-week class
7133 // noThemeHighlight=true (don't highlight the header)
7134 this.getDayClasses(date
, true)
7138 classNames
.push('fc-' + dayIDs
[date
.day()]); // only add the day-of-week class
7142 '<th class="' + classNames
.join(' ') + '"' +
7143 ((isDateValid
&& this.rowCnt
) === 1 ?
7144 ' data-date="' + date
.format('YYYY-MM-DD') + '"' :
7147 ' colspan="' + colspan
+ '"' :
7154 // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
7155 view
.buildGotoAnchorHtml(
7156 { date
: date
, forceOff
: this.rowCnt
> 1 || this.colCnt
=== 1 },
7159 // if not valid, display text, but no link
7166 /* Background Rendering
7167 ------------------------------------------------------------------------------------------------------------------*/
7170 renderBgTrHtml: function(row
) {
7173 (this.isRTL
? '' : this.renderBgIntroHtml(row
)) +
7174 this.renderBgCellsHtml(row
) +
7175 (this.isRTL
? this.renderBgIntroHtml(row
) : '') +
7180 renderBgIntroHtml: function(row
) {
7181 return this.renderIntroHtml(); // fall back to generic
7185 renderBgCellsHtml: function(row
) {
7189 for (col
= 0; col
< this.colCnt
; col
++) {
7190 date
= this.getCellDate(row
, col
);
7191 htmls
.push(this.renderBgCellHtml(date
));
7194 return htmls
.join('');
7198 renderBgCellHtml: function(date
, otherAttrs
) {
7199 var view
= this.view
;
7200 var isDateValid
= view
.activeUnzonedRange
.containsDate(date
); // TODO: called too frequently. cache somehow.
7201 var classes
= this.getDayClasses(date
);
7203 classes
.unshift('fc-day', view
.calendar
.theme
.getClass('widgetContent'));
7205 return '<td class="' + classes
.join(' ') + '"' +
7207 ' data-date="' + date
.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
7217 ------------------------------------------------------------------------------------------------------------------*/
7220 // Generates the default HTML intro for any row. User classes should override
7221 renderIntroHtml: function() {
7225 // TODO: a generic method for dealing with <tr>, RTL, intro
7226 // when increment internalApiVersion
7227 // wrapTr (scheduler)
7231 ------------------------------------------------------------------------------------------------------------------*/
7234 // Applies the generic "intro" and "outro" HTML to the given cells.
7235 // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
7236 bookendCells: function(trEl
) {
7237 var introHtml
= this.renderIntroHtml();
7241 trEl
.append(introHtml
);
7244 trEl
.prepend(introHtml
);
7253 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
7254 ----------------------------------------------------------------------------------------------------------------------*/
7256 var DayGrid
= FC
.DayGrid
= Grid
.extend(DayTableMixin
, {
7258 numbersVisible
: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
7259 bottomCoordPadding
: 0, // hack for extending the hit area for the last row of the coordinate grid
7261 rowEls
: null, // set of fake row elements
7262 cellEls
: null, // set of whole-day elements comprising the row's background
7263 helperEls
: null, // set of cell skeleton elements for rendering the mock event "helper"
7265 rowCoordCache
: null,
7266 colCoordCache
: null,
7269 // Renders the rows and columns into the component's `this.el`, which should already be assigned.
7270 // isRigid determins whether the individual rows should ignore the contents and be a constant height.
7271 // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
7272 renderDates: function(isRigid
) {
7273 var view
= this.view
;
7274 var rowCnt
= this.rowCnt
;
7275 var colCnt
= this.colCnt
;
7280 for (row
= 0; row
< rowCnt
; row
++) {
7281 html
+= this.renderDayRowHtml(row
, isRigid
);
7285 this.rowEls
= this.el
.find('.fc-row');
7286 this.cellEls
= this.el
.find('.fc-day, .fc-disabled-day');
7288 this.rowCoordCache
= new CoordCache({
7292 this.colCoordCache
= new CoordCache({
7293 els
: this.cellEls
.slice(0, this.colCnt
), // only the first row
7297 // trigger dayRender with each cell's element
7298 for (row
= 0; row
< rowCnt
; row
++) {
7299 for (col
= 0; col
< colCnt
; col
++) {
7300 this.publiclyTrigger('dayRender', {
7303 this.getCellDate(row
, col
),
7304 this.getCellEl(row
, col
),
7313 unrenderDates: function() {
7314 this.removeSegPopover();
7318 renderBusinessHours: function() {
7319 var segs
= this.buildBusinessHourSegs(true); // wholeDay=true
7320 this.renderFill('businessHours', segs
, 'bgevent');
7324 unrenderBusinessHours: function() {
7325 this.unrenderFill('businessHours');
7329 // Generates the HTML for a single row, which is a div that wraps a table.
7330 // `row` is the row number.
7331 renderDayRowHtml: function(row
, isRigid
) {
7332 var theme
= this.view
.calendar
.theme
;
7333 var classes
= [ 'fc-row', 'fc-week', theme
.getClass('dayRow') ];
7336 classes
.push('fc-rigid');
7340 '<div class="' + classes
.join(' ') + '">' +
7341 '<div class="fc-bg">' +
7342 '<table class="' + theme
.getClass('tableGrid') + '">' +
7343 this.renderBgTrHtml(row
) +
7346 '<div class="fc-content-skeleton">' +
7348 (this.numbersVisible
?
7350 this.renderNumberTrHtml(row
) +
7360 /* Grid Number Rendering
7361 ------------------------------------------------------------------------------------------------------------------*/
7364 renderNumberTrHtml: function(row
) {
7367 (this.isRTL
? '' : this.renderNumberIntroHtml(row
)) +
7368 this.renderNumberCellsHtml(row
) +
7369 (this.isRTL
? this.renderNumberIntroHtml(row
) : '') +
7374 renderNumberIntroHtml: function(row
) {
7375 return this.renderIntroHtml();
7379 renderNumberCellsHtml: function(row
) {
7383 for (col
= 0; col
< this.colCnt
; col
++) {
7384 date
= this.getCellDate(row
, col
);
7385 htmls
.push(this.renderNumberCellHtml(date
));
7388 return htmls
.join('');
7392 // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
7393 // The number row will only exist if either day numbers or week numbers are turned on.
7394 renderNumberCellHtml: function(date
) {
7395 var view
= this.view
;
7397 var isDateValid
= view
.activeUnzonedRange
.containsDate(date
); // TODO: called too frequently. cache somehow.
7398 var isDayNumberVisible
= view
.dayNumbersVisible
&& isDateValid
;
7400 var weekCalcFirstDoW
;
7402 if (!isDayNumberVisible
&& !view
.cellWeekNumbersVisible
) {
7403 // no numbers in day cell (week number must be along the side)
7404 return '<td/>'; // will create an empty space above events :(
7407 classes
= this.getDayClasses(date
);
7408 classes
.unshift('fc-day-top');
7410 if (view
.cellWeekNumbersVisible
) {
7411 // To determine the day of week number change under ISO, we cannot
7412 // rely on moment.js methods such as firstDayOfWeek() or weekday(),
7413 // because they rely on the locale's dow (possibly overridden by
7414 // our firstDay option), which may not be Monday. We cannot change
7415 // dow, because that would affect the calendar start day as well.
7416 if (date
._locale
._fullCalendar_weekCalc
=== 'ISO') {
7417 weekCalcFirstDoW
= 1; // Monday by ISO 8601 definition
7420 weekCalcFirstDoW
= date
._locale
.firstDayOfWeek();
7424 html
+= '<td class="' + classes
.join(' ') + '"' +
7426 ' data-date="' + date
.format() + '"' :
7431 if (view
.cellWeekNumbersVisible
&& (date
.day() == weekCalcFirstDoW
)) {
7432 html
+= view
.buildGotoAnchorHtml(
7433 { date
: date
, type
: 'week' },
7434 { 'class': 'fc-week-number' },
7435 date
.format('w') // inner HTML
7439 if (isDayNumberVisible
) {
7440 html
+= view
.buildGotoAnchorHtml(
7442 { 'class': 'fc-day-number' },
7443 date
.date() // inner HTML
7454 ------------------------------------------------------------------------------------------------------------------*/
7457 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
7458 computeEventTimeFormat: function() {
7459 return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
7463 // Computes a default `displayEventEnd` value if one is not expliclty defined
7464 computeDisplayEventEnd: function() {
7465 return this.colCnt
== 1; // we'll likely have space if there's only one day
7470 ------------------------------------------------------------------------------------------------------------------*/
7473 rangeUpdated: function() {
7474 this.updateDayTable();
7478 // Slices up the given span (unzoned start/end with other misc data) into an array of segments
7479 componentFootprintToSegs: function(componentFootprint
) {
7480 var segs
= this.sliceRangeByRow(componentFootprint
.unzonedRange
);
7483 for (i
= 0; i
< segs
.length
; i
++) {
7487 seg
.leftCol
= this.daysPerRow
- 1 - seg
.lastRowDayIndex
;
7488 seg
.rightCol
= this.daysPerRow
- 1 - seg
.firstRowDayIndex
;
7491 seg
.leftCol
= seg
.firstRowDayIndex
;
7492 seg
.rightCol
= seg
.lastRowDayIndex
;
7501 ------------------------------------------------------------------------------------------------------------------*/
7504 prepareHits: function() {
7505 this.colCoordCache
.build();
7506 this.rowCoordCache
.build();
7507 this.rowCoordCache
.bottoms
[this.rowCnt
- 1] += this.bottomCoordPadding
; // hack
7511 releaseHits: function() {
7512 this.colCoordCache
.clear();
7513 this.rowCoordCache
.clear();
7517 queryHit: function(leftOffset
, topOffset
) {
7518 if (this.colCoordCache
.isLeftInBounds(leftOffset
) && this.rowCoordCache
.isTopInBounds(topOffset
)) {
7519 var col
= this.colCoordCache
.getHorizontalIndex(leftOffset
);
7520 var row
= this.rowCoordCache
.getVerticalIndex(topOffset
);
7522 if (row
!= null && col
!= null) {
7523 return this.getCellHit(row
, col
);
7529 getHitFootprint: function(hit
) {
7530 var range
= this.getCellRange(hit
.row
, hit
.col
);
7532 return new ComponentFootprint(
7533 new UnzonedRange(range
.start
, range
.end
),
7539 getHitEl: function(hit
) {
7540 return this.getCellEl(hit
.row
, hit
.col
);
7545 ------------------------------------------------------------------------------------------------------------------*/
7546 // FYI: the first column is the leftmost column, regardless of date
7549 getCellHit: function(row
, col
) {
7553 component
: this, // needed unfortunately :(
7554 left
: this.colCoordCache
.getLeftOffset(col
),
7555 right
: this.colCoordCache
.getRightOffset(col
),
7556 top
: this.rowCoordCache
.getTopOffset(row
),
7557 bottom
: this.rowCoordCache
.getBottomOffset(row
)
7562 getCellEl: function(row
, col
) {
7563 return this.cellEls
.eq(row
* this.colCnt
+ col
);
7567 /* Event Drag Visualization
7568 ------------------------------------------------------------------------------------------------------------------*/
7569 // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
7572 // Renders a visual indication of an event or external element being dragged.
7573 // `eventLocation` has zoned start and end (optional)
7574 renderDrag: function(eventFootprints
, seg
) {
7577 for (i
= 0; i
< eventFootprints
.length
; i
++) {
7578 this.renderHighlight(eventFootprints
[i
].componentFootprint
);
7581 // if a segment from the same calendar but another component is being dragged, render a helper event
7582 if (seg
&& seg
.component
!== this) {
7583 return this.renderHelperEventFootprints(eventFootprints
, seg
); // returns mock event elements
7588 // Unrenders any visual indication of a hovering event
7589 unrenderDrag: function() {
7590 this.unrenderHighlight();
7591 this.unrenderHelper();
7595 /* Event Resize Visualization
7596 ------------------------------------------------------------------------------------------------------------------*/
7599 // Renders a visual indication of an event being resized
7600 renderEventResize: function(eventFootprints
, seg
) {
7603 for (i
= 0; i
< eventFootprints
.length
; i
++) {
7604 this.renderHighlight(eventFootprints
[i
].componentFootprint
);
7607 return this.renderHelperEventFootprints(eventFootprints
, seg
); // returns mock event elements
7611 // Unrenders a visual indication of an event being resized
7612 unrenderEventResize: function() {
7613 this.unrenderHighlight();
7614 this.unrenderHelper();
7619 ------------------------------------------------------------------------------------------------------------------*/
7622 // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
7623 renderHelperEventFootprintEls: function(eventFootprints
, sourceSeg
) {
7624 var helperNodes
= [];
7625 var segs
= this.eventFootprintsToSegs(eventFootprints
);
7628 segs
= this.renderFgSegEls(segs
); // assigns each seg's el and returns a subset of segs that were rendered
7629 rowStructs
= this.renderSegRows(segs
);
7631 // inject each new event skeleton into each associated row
7632 this.rowEls
.each(function(row
, rowNode
) {
7633 var rowEl
= $(rowNode
); // the .fc-row
7634 var skeletonEl
= $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
7637 // If there is an original segment, match the top position. Otherwise, put it at the row's top level
7638 if (sourceSeg
&& sourceSeg
.row
=== row
) {
7639 skeletonTop
= sourceSeg
.el
.position().top
;
7642 skeletonTop
= rowEl
.find('.fc-content-skeleton tbody').position().top
;
7645 skeletonEl
.css('top', skeletonTop
)
7647 .append(rowStructs
[row
].tbodyEl
);
7649 rowEl
.append(skeletonEl
);
7650 helperNodes
.push(skeletonEl
[0]);
7653 return ( // must return the elements rendered
7654 this.helperEls
= $(helperNodes
) // array -> jQuery set
7659 // Unrenders any visual indication of a mock helper event
7660 unrenderHelper: function() {
7661 if (this.helperEls
) {
7662 this.helperEls
.remove();
7663 this.helperEls
= null;
7668 /* Fill System (highlight, background events, business hours)
7669 ------------------------------------------------------------------------------------------------------------------*/
7672 fillSegTag
: 'td', // override the default tag name
7675 // Renders a set of rectangles over the given segments of days.
7676 // Only returns segments that successfully rendered.
7677 renderFill: function(type
, segs
, className
) {
7682 segs
= this.renderFillSegEls(type
, segs
); // assignes `.el` to each seg. returns successfully rendered segs
7684 for (i
= 0; i
< segs
.length
; i
++) {
7686 skeletonEl
= this.renderFillRow(type
, seg
, className
);
7687 this.rowEls
.eq(seg
.row
).append(skeletonEl
);
7688 nodes
.push(skeletonEl
[0]);
7691 if (this.elsByFill
[type
]) {
7692 this.elsByFill
[type
] = this.elsByFill
[type
].add(nodes
);
7695 this.elsByFill
[type
] = $(nodes
);
7702 // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
7703 renderFillRow: function(type
, seg
, className
) {
7704 var colCnt
= this.colCnt
;
7705 var startCol
= seg
.leftCol
;
7706 var endCol
= seg
.rightCol
+ 1;
7710 className
= className
|| type
.toLowerCase();
7713 '<div class="fc-' + className
+ '-skeleton">' +
7714 '<table><tr/></table>' +
7717 trEl
= skeletonEl
.find('tr');
7720 trEl
.append('<td colspan="' + startCol
+ '"/>');
7724 seg
.el
.attr('colspan', endCol
- startCol
)
7727 if (endCol
< colCnt
) {
7728 trEl
.append('<td colspan="' + (colCnt
- endCol
) + '"/>');
7731 this.bookendCells(trEl
);
7740 /* Event-rendering methods for the DayGrid class
7741 ----------------------------------------------------------------------------------------------------------------------*/
7745 rowStructs
: null, // an array of objects, each holding information about a row's foreground event-rendering
7748 // Unrenders all events currently rendered on the grid
7749 unrenderEvents: function() {
7750 this.removeSegPopover(); // removes the "more.." events popover
7751 Grid
.prototype.unrenderEvents
.apply(this, arguments
); // calls the super-method
7755 // Retrieves all rendered segment objects currently rendered on the grid
7756 getEventSegs: function() {
7757 return Grid
.prototype.getEventSegs
.call(this) // get the segments from the super-method
7758 .concat(this.popoverSegs
|| []); // append the segments from the "more..." popover
7762 // Renders the given background event segments onto the grid
7763 renderBgSegs: function(segs
) {
7765 // don't render timed background events
7766 var allDaySegs
= $.grep(segs
, function(seg
) {
7767 return seg
.footprint
.componentFootprint
.isAllDay
;
7770 return Grid
.prototype.renderBgSegs
.call(this, allDaySegs
); // call the super-method
7774 // Renders the given foreground event segments onto the grid
7775 renderFgSegs: function(segs
) {
7778 // render an `.el` on each seg
7779 // returns a subset of the segs. segs that were actually rendered
7780 segs
= this.renderFgSegEls(segs
);
7782 rowStructs
= this.rowStructs
= this.renderSegRows(segs
);
7784 // append to each row's content skeleton
7785 this.rowEls
.each(function(i
, rowNode
) {
7786 $(rowNode
).find('.fc-content-skeleton > table').append(
7787 rowStructs
[i
].tbodyEl
7791 return segs
; // return only the segs that were actually rendered
7795 // Unrenders all currently rendered foreground event segments
7796 unrenderFgSegs: function() {
7797 var rowStructs
= this.rowStructs
|| [];
7800 while ((rowStruct
= rowStructs
.pop())) {
7801 rowStruct
.tbodyEl
.remove();
7804 this.rowStructs
= null;
7808 // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
7809 // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
7810 // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
7811 renderSegRows: function(segs
) {
7812 var rowStructs
= [];
7816 segRows
= this.groupSegRows(segs
); // group into nested arrays
7818 // iterate each row of segment groupings
7819 for (row
= 0; row
< segRows
.length
; row
++) {
7821 this.renderSegRow(row
, segRows
[row
])
7829 // Builds the HTML to be used for the default element for an individual segment
7830 fgSegHtml: function(seg
, disableResizing
) {
7831 var view
= this.view
;
7832 var eventDef
= seg
.footprint
.eventDef
;
7833 var isAllDay
= seg
.footprint
.componentFootprint
.isAllDay
;
7834 var isDraggable
= view
.isEventDefDraggable(eventDef
);
7835 var isResizableFromStart
= !disableResizing
&& isAllDay
&&
7836 seg
.isStart
&& view
.isEventDefResizableFromStart(eventDef
);
7837 var isResizableFromEnd
= !disableResizing
&& isAllDay
&&
7838 seg
.isEnd
&& view
.isEventDefResizableFromEnd(eventDef
);
7839 var classes
= this.getSegClasses(seg
, isDraggable
, isResizableFromStart
|| isResizableFromEnd
);
7840 var skinCss
= cssToStr(this.getSegSkinCss(seg
));
7845 classes
.unshift('fc-day-grid-event', 'fc-h-event');
7847 // Only display a timed events time if it is the starting segment
7849 timeText
= this.getEventTimeText(seg
.footprint
);
7851 timeHtml
= '<span class="fc-time">' + htmlEscape(timeText
) + '</span>';
7856 '<span class="fc-title">' +
7857 (htmlEscape(eventDef
.title
|| '') || ' ') + // we always want one line of height
7860 return '<a class="' + classes
.join(' ') + '"' +
7862 ' href="' + htmlEscape(eventDef
.url
) + '"' :
7866 ' style="' + skinCss
+ '"' :
7870 '<div class="fc-content">' +
7872 titleHtml
+ ' ' + timeHtml
: // put a natural space in between
7873 timeHtml
+ ' ' + titleHtml
//
7876 (isResizableFromStart
?
7877 '<div class="fc-resizer fc-start-resizer" />' :
7880 (isResizableFromEnd
?
7881 '<div class="fc-resizer fc-end-resizer" />' :
7888 // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
7889 // the segments. Returns object with a bunch of internal data about how the render was calculated.
7890 // NOTE: modifies rowSegs
7891 renderSegRow: function(row
, rowSegs
) {
7892 var colCnt
= this.colCnt
;
7893 var segLevels
= this.buildSegLevels(rowSegs
); // group into sub-arrays of levels
7894 var levelCnt
= Math
.max(1, segLevels
.length
); // ensure at least one level
7895 var tbody
= $('<tbody/>');
7896 var segMatrix
= []; // lookup for which segments are rendered into which level+col cells
7897 var cellMatrix
= []; // lookup for all <td> elements of the level+col matrix
7898 var loneCellMatrix
= []; // lookup for <td> elements that only take up a single column
7905 // populates empty cells from the current column (`col`) to `endCol`
7906 function emptyCellsUntil(endCol
) {
7907 while (col
< endCol
) {
7908 // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
7909 td
= (loneCellMatrix
[i
- 1] || [])[col
];
7913 parseInt(td
.attr('rowspan') || 1, 10) + 1
7920 cellMatrix
[i
][col
] = td
;
7921 loneCellMatrix
[i
][col
] = td
;
7926 for (i
= 0; i
< levelCnt
; i
++) { // iterate through all levels
7927 levelSegs
= segLevels
[i
];
7932 cellMatrix
.push([]);
7933 loneCellMatrix
.push([]);
7935 // levelCnt might be 1 even though there are no actual levels. protect against this.
7936 // this single empty row is useful for styling.
7938 for (j
= 0; j
< levelSegs
.length
; j
++) { // iterate through segments in level
7941 emptyCellsUntil(seg
.leftCol
);
7943 // create a container that occupies or more columns. append the event element.
7944 td
= $('<td class="fc-event-container"/>').append(seg
.el
);
7945 if (seg
.leftCol
!= seg
.rightCol
) {
7946 td
.attr('colspan', seg
.rightCol
- seg
.leftCol
+ 1);
7948 else { // a single-column segment
7949 loneCellMatrix
[i
][col
] = td
;
7952 while (col
<= seg
.rightCol
) {
7953 cellMatrix
[i
][col
] = td
;
7954 segMatrix
[i
][col
] = seg
;
7962 emptyCellsUntil(colCnt
); // finish off the row
7963 this.bookendCells(tr
);
7967 return { // a "rowStruct"
7968 row
: row
, // the row number
7970 cellMatrix
: cellMatrix
,
7971 segMatrix
: segMatrix
,
7972 segLevels
: segLevels
,
7978 // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
7979 // NOTE: modifies segs
7980 buildSegLevels: function(segs
) {
7985 // Give preference to elements with certain criteria, so they have
7986 // a chance to be closer to the top.
7987 this.sortEventSegs(segs
);
7989 for (i
= 0; i
< segs
.length
; i
++) {
7992 // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
7993 for (j
= 0; j
< levels
.length
; j
++) {
7994 if (!isDaySegCollision(seg
, levels
[j
])) {
7998 // `j` now holds the desired subrow index
8001 // create new level array if needed and append segment
8002 (levels
[j
] || (levels
[j
] = [])).push(seg
);
8005 // order segments left-to-right. very important if calendar is RTL
8006 for (j
= 0; j
< levels
.length
; j
++) {
8007 levels
[j
].sort(compareDaySegCols
);
8014 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
8015 groupSegRows: function(segs
) {
8019 for (i
= 0; i
< this.rowCnt
; i
++) {
8023 for (i
= 0; i
< segs
.length
; i
++) {
8024 segRows
[segs
[i
].row
].push(segs
[i
]);
8033 // Computes whether two segments' columns collide. They are assumed to be in the same row.
8034 function isDaySegCollision(seg
, otherSegs
) {
8037 for (i
= 0; i
< otherSegs
.length
; i
++) {
8038 otherSeg
= otherSegs
[i
];
8041 otherSeg
.leftCol
<= seg
.rightCol
&&
8042 otherSeg
.rightCol
>= seg
.leftCol
8052 // A cmp function for determining the leftmost event
8053 function compareDaySegCols(a
, b
) {
8054 return a
.leftCol
- b
.leftCol
;
8059 /* Methods relate to limiting the number events for a given day on a DayGrid
8060 ----------------------------------------------------------------------------------------------------------------------*/
8061 // NOTE: all the segs being passed around in here are foreground segs
8065 segPopover
: null, // the Popover that holds events that can't fit in a cell. null when not visible
8066 popoverSegs
: null, // an array of segment objects that the segPopover holds. null when not visible
8069 removeSegPopover: function() {
8070 if (this.segPopover
) {
8071 this.segPopover
.hide(); // in handler, will call segPopover's removeElement
8076 // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
8077 // `levelLimit` can be false (don't limit), a number, or true (should be computed).
8078 limitRows: function(levelLimit
) {
8079 var rowStructs
= this.rowStructs
|| [];
8083 for (row
= 0; row
< rowStructs
.length
; row
++) {
8084 this.unlimitRow(row
);
8087 rowLevelLimit
= false;
8089 else if (typeof levelLimit
=== 'number') {
8090 rowLevelLimit
= levelLimit
;
8093 rowLevelLimit
= this.computeRowLevelLimit(row
);
8096 if (rowLevelLimit
!== false) {
8097 this.limitRow(row
, rowLevelLimit
);
8103 // Computes the number of levels a row will accomodate without going outside its bounds.
8104 // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
8105 // `row` is the row number.
8106 computeRowLevelLimit: function(row
) {
8107 var rowEl
= this.rowEls
.eq(row
); // the containing "fake" row div
8108 var rowHeight
= rowEl
.height(); // TODO: cache somehow?
8109 var trEls
= this.rowStructs
[row
].tbodyEl
.children();
8113 function iterInnerHeights(i
, childNode
) {
8114 trHeight
= Math
.max(trHeight
, $(childNode
).outerHeight());
8117 // Reveal one level <tr> at a time and stop when we find one out of bounds
8118 for (i
= 0; i
< trEls
.length
; i
++) {
8119 trEl
= trEls
.eq(i
).removeClass('fc-limited'); // reset to original state (reveal)
8121 // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
8122 // so instead, find the tallest inner content element.
8124 trEl
.find('> td > :first-child').each(iterInnerHeights
);
8126 if (trEl
.position().top
+ trHeight
> rowHeight
) {
8131 return false; // should not limit at all
8135 // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
8136 // `row` is the row number.
8137 // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
8138 limitRow: function(row
, levelLimit
) {
8140 var rowStruct
= this.rowStructs
[row
];
8141 var moreNodes
= []; // array of "more" <a> links and <td> DOM nodes
8142 var col
= 0; // col #, left-to-right (not chronologically)
8143 var levelSegs
; // array of segment objects in the last allowable level, ordered left-to-right
8144 var cellMatrix
; // a matrix (by level, then column) of all <td> jQuery elements in the row
8145 var limitedNodes
; // array of temporarily hidden level <tr> and segment <td> DOM nodes
8147 var segsBelow
; // array of segment objects below `seg` in the current `col`
8148 var totalSegsBelow
; // total number of segments below `seg` in any of the columns `seg` occupies
8149 var colSegsBelow
; // array of segment arrays, below seg, one for each column (offset from segs's first column)
8151 var segMoreNodes
; // array of "more" <td> cells that will stand-in for the current seg's cell
8153 var moreTd
, moreWrap
, moreLink
;
8155 // Iterates through empty level cells and places "more" links inside if need be
8156 function emptyCellsUntil(endCol
) { // goes from current `col` to `endCol`
8157 while (col
< endCol
) {
8158 segsBelow
= _this
.getCellSegs(row
, col
, levelLimit
);
8159 if (segsBelow
.length
) {
8160 td
= cellMatrix
[levelLimit
- 1][col
];
8161 moreLink
= _this
.renderMoreLink(row
, col
, segsBelow
);
8162 moreWrap
= $('<div/>').append(moreLink
);
8163 td
.append(moreWrap
);
8164 moreNodes
.push(moreWrap
[0]);
8170 if (levelLimit
&& levelLimit
< rowStruct
.segLevels
.length
) { // is it actually over the limit?
8171 levelSegs
= rowStruct
.segLevels
[levelLimit
- 1];
8172 cellMatrix
= rowStruct
.cellMatrix
;
8174 limitedNodes
= rowStruct
.tbodyEl
.children().slice(levelLimit
) // get level <tr> elements past the limit
8175 .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
8177 // iterate though segments in the last allowable level
8178 for (i
= 0; i
< levelSegs
.length
; i
++) {
8180 emptyCellsUntil(seg
.leftCol
); // process empty cells before the segment
8182 // determine *all* segments below `seg` that occupy the same columns
8185 while (col
<= seg
.rightCol
) {
8186 segsBelow
= this.getCellSegs(row
, col
, levelLimit
);
8187 colSegsBelow
.push(segsBelow
);
8188 totalSegsBelow
+= segsBelow
.length
;
8192 if (totalSegsBelow
) { // do we need to replace this segment with one or many "more" links?
8193 td
= cellMatrix
[levelLimit
- 1][seg
.leftCol
]; // the segment's parent cell
8194 rowspan
= td
.attr('rowspan') || 1;
8197 // make a replacement <td> for each column the segment occupies. will be one for each colspan
8198 for (j
= 0; j
< colSegsBelow
.length
; j
++) {
8199 moreTd
= $('<td class="fc-more-cell"/>').attr('rowspan', rowspan
);
8200 segsBelow
= colSegsBelow
[j
];
8201 moreLink
= this.renderMoreLink(
8204 [ seg
].concat(segsBelow
) // count seg as hidden too
8206 moreWrap
= $('<div/>').append(moreLink
);
8207 moreTd
.append(moreWrap
);
8208 segMoreNodes
.push(moreTd
[0]);
8209 moreNodes
.push(moreTd
[0]);
8212 td
.addClass('fc-limited').after($(segMoreNodes
)); // hide original <td> and inject replacements
8213 limitedNodes
.push(td
[0]);
8217 emptyCellsUntil(this.colCnt
); // finish off the level
8218 rowStruct
.moreEls
= $(moreNodes
); // for easy undoing later
8219 rowStruct
.limitedEls
= $(limitedNodes
); // for easy undoing later
8224 // Reveals all levels and removes all "more"-related elements for a grid's row.
8225 // `row` is a row number.
8226 unlimitRow: function(row
) {
8227 var rowStruct
= this.rowStructs
[row
];
8229 if (rowStruct
.moreEls
) {
8230 rowStruct
.moreEls
.remove();
8231 rowStruct
.moreEls
= null;
8234 if (rowStruct
.limitedEls
) {
8235 rowStruct
.limitedEls
.removeClass('fc-limited');
8236 rowStruct
.limitedEls
= null;
8241 // Renders an <a> element that represents hidden event element for a cell.
8242 // Responsible for attaching click handler as well.
8243 renderMoreLink: function(row
, col
, hiddenSegs
) {
8245 var view
= this.view
;
8247 return $('<a class="fc-more"/>')
8249 this.getMoreLinkText(hiddenSegs
.length
)
8251 .on('click', function(ev
) {
8252 var clickOption
= _this
.opt('eventLimitClick');
8253 var date
= _this
.getCellDate(row
, col
);
8254 var moreEl
= $(this);
8255 var dayEl
= _this
.getCellEl(row
, col
);
8256 var allSegs
= _this
.getCellSegs(row
, col
);
8258 // rescope the segments to be within the cell's date
8259 var reslicedAllSegs
= _this
.resliceDaySegs(allSegs
, date
);
8260 var reslicedHiddenSegs
= _this
.resliceDaySegs(hiddenSegs
, date
);
8262 if (typeof clickOption
=== 'function') {
8263 // the returned value can be an atomic option
8264 clickOption
= _this
.publiclyTrigger('eventLimitClick', {
8271 segs
: reslicedAllSegs
,
8272 hiddenSegs
: reslicedHiddenSegs
8280 if (clickOption
=== 'popover') {
8281 _this
.showSegPopover(row
, col
, moreEl
, reslicedAllSegs
);
8283 else if (typeof clickOption
=== 'string') { // a view name
8284 view
.calendar
.zoomTo(date
, clickOption
);
8290 // Reveals the popover that displays all events within a cell
8291 showSegPopover: function(row
, col
, moreLink
, segs
) {
8293 var view
= this.view
;
8294 var moreWrap
= moreLink
.parent(); // the <div> wrapper around the <a>
8295 var topEl
; // the element we want to match the top coordinate of
8298 if (this.rowCnt
== 1) {
8299 topEl
= view
.el
; // will cause the popover to cover any sort of header
8302 topEl
= this.rowEls
.eq(row
); // will align with top of row
8306 className
: 'fc-more-popover ' + view
.calendar
.theme
.getClass('popover'),
8307 content
: this.renderSegPopoverContent(row
, col
, segs
),
8308 parentEl
: view
.el
, // attach to root of view. guarantees outside of scrollbars.
8309 top
: topEl
.offset().top
,
8310 autoHide
: true, // when the user clicks elsewhere, hide the popover
8311 viewportConstrain
: this.opt('popoverViewportConstrain'),
8313 // kill everything when the popover is hidden
8314 // notify events to be removed
8315 if (_this
.popoverSegs
) {
8320 for (i
= 0; i
< _this
.popoverSegs
.length
; ++i
) {
8321 seg
= _this
.popoverSegs
[i
];
8322 legacy
= seg
.footprint
.getEventLegacy();
8324 _this
.publiclyTrigger('eventDestroy', {
8326 args
: [ legacy
, seg
.el
, view
]
8330 _this
.segPopover
.removeElement();
8331 _this
.segPopover
= null;
8332 _this
.popoverSegs
= null;
8336 // Determine horizontal coordinate.
8337 // We use the moreWrap instead of the <td> to avoid border confusion.
8339 options
.right
= moreWrap
.offset().left
+ moreWrap
.outerWidth() + 1; // +1 to be over cell border
8342 options
.left
= moreWrap
.offset().left
- 1; // -1 to be over cell border
8345 this.segPopover
= new Popover(options
);
8346 this.segPopover
.show();
8348 // the popover doesn't live within the grid's container element, and thus won't get the event
8349 // delegated-handlers for free. attach event-related handlers to the popover.
8350 this.bindSegHandlersToEl(this.segPopover
.el
);
8354 // Builds the inner DOM contents of the segment popover
8355 renderSegPopoverContent: function(row
, col
, segs
) {
8356 var view
= this.view
;
8357 var theme
= view
.calendar
.theme
;
8358 var title
= this.getCellDate(row
, col
).format(this.opt('dayPopoverFormat'));
8360 '<div class="fc-header ' + theme
.getClass('popoverHeader') + '">' +
8361 '<span class="fc-close ' + theme
.getIconClass('close') + '"></span>' +
8362 '<span class="fc-title">' +
8365 '<div class="fc-clear"/>' +
8367 '<div class="fc-body ' + theme
.getClass('popoverContent') + '">' +
8368 '<div class="fc-event-container"></div>' +
8371 var segContainer
= content
.find('.fc-event-container');
8374 // render each seg's `el` and only return the visible segs
8375 segs
= this.renderFgSegEls(segs
, true); // disableResizing=true
8376 this.popoverSegs
= segs
;
8378 for (i
= 0; i
< segs
.length
; i
++) {
8380 // because segments in the popover are not part of a grid coordinate system, provide a hint to any
8381 // grids that want to do drag-n-drop about which cell it came from
8383 segs
[i
].hit
= this.getCellHit(row
, col
);
8384 this.hitsNotNeeded();
8386 segContainer
.append(segs
[i
].el
);
8393 // Given the events within an array of segment objects, reslice them to be in a single day
8394 resliceDaySegs: function(segs
, dayDate
) {
8395 var dayStart
= dayDate
.clone();
8396 var dayEnd
= dayStart
.clone().add(1, 'days');
8397 var dayRange
= new UnzonedRange(dayStart
, dayEnd
);
8401 for (i
= 0; i
< segs
.length
; i
++) {
8402 newSegs
.push
.apply(newSegs
, // append
8403 this.eventFootprintToSegs(segs
[i
].footprint
, dayRange
)
8407 // force an order because eventsToSegs doesn't guarantee one
8408 // TODO: research if still needed
8409 this.sortEventSegs(newSegs
);
8415 // Generates the text that should be inside a "more" link, given the number of events it represents
8416 getMoreLinkText: function(num
) {
8417 var opt
= this.opt('eventLimitText');
8419 if (typeof opt
=== 'function') {
8423 return '+' + num
+ ' ' + opt
;
8428 // Returns segments within a given cell.
8429 // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
8430 getCellSegs: function(row
, col
, startLevel
) {
8431 var segMatrix
= this.rowStructs
[row
].segMatrix
;
8432 var level
= startLevel
|| 0;
8436 while (level
< segMatrix
.length
) {
8437 seg
= segMatrix
[level
][col
];
8451 /* A component that renders one or more columns of vertical time slots
8452 ----------------------------------------------------------------------------------------------------------------------*/
8453 // We mixin DayTable, even though there is only a single row of days
8455 var TimeGrid
= FC
.TimeGrid
= Grid
.extend(DayTableMixin
, {
8457 dayRanges
: null, // UnzonedRange[], of start-end of each day
8458 slotDuration
: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
8459 snapDuration
: null, // granularity of time for dragging and selecting
8461 labelFormat
: null, // formatting string for times running along vertical axis
8462 labelInterval
: null, // duration of how often a label should be displayed for a slot
8464 colEls
: null, // cells elements in the day-row background
8465 slatContainerEl
: null, // div that wraps all the slat rows
8466 slatEls
: null, // elements running horizontally across all columns
8467 nowIndicatorEls
: null,
8469 colCoordCache
: null,
8470 slatCoordCache
: null,
8473 constructor: function() {
8474 Grid
.apply(this, arguments
); // call the super-constructor
8476 this.processOptions();
8480 // Renders the time grid into `this.el`, which should already be assigned.
8481 // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
8482 renderDates: function() {
8483 this.el
.html(this.renderHtml());
8484 this.colEls
= this.el
.find('.fc-day, .fc-disabled-day');
8485 this.slatContainerEl
= this.el
.find('.fc-slats');
8486 this.slatEls
= this.slatContainerEl
.find('tr');
8488 this.colCoordCache
= new CoordCache({
8492 this.slatCoordCache
= new CoordCache({
8497 this.renderContentSkeleton();
8501 // Renders the basic HTML skeleton for the grid
8502 renderHtml: function() {
8503 var theme
= this.view
.calendar
.theme
;
8506 '<div class="fc-bg">' +
8507 '<table class="' + theme
.getClass('tableGrid') + '">' +
8508 this.renderBgTrHtml(0) + // row=0
8511 '<div class="fc-slats">' +
8512 '<table class="' + theme
.getClass('tableGrid') + '">' +
8513 this.renderSlatRowHtml() +
8519 // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
8520 renderSlatRowHtml: function() {
8521 var view
= this.view
;
8522 var calendar
= view
.calendar
;
8523 var theme
= calendar
.theme
;
8524 var isRTL
= this.isRTL
;
8526 var slotTime
= moment
.duration(+this.view
.minTime
); // wish there was .clone() for durations
8527 var slotIterator
= moment
.duration(0);
8528 var slotDate
; // will be on the view's first day, but we only care about its time
8532 // Calculate the time for each slot
8533 while (slotTime
< view
.maxTime
) {
8534 slotDate
= calendar
.msToUtcMoment(this.unzonedRange
.startMs
).time(slotTime
);
8535 isLabeled
= isInt(divideDurationByDuration(slotIterator
, this.labelInterval
));
8538 '<td class="fc-axis fc-time ' + theme
.getClass('widgetContent') + '" ' + view
.axisStyleAttr() + '>' +
8540 '<span>' + // for matchCellWidths
8541 htmlEscape(slotDate
.format(this.labelFormat
)) +
8548 '<tr data-time="' + slotDate
.format('HH:mm:ss') + '"' +
8549 (isLabeled
? '' : ' class="fc-minor"') +
8551 (!isRTL
? axisHtml
: '') +
8552 '<td class="' + theme
.getClass('widgetContent') + '"/>' +
8553 (isRTL
? axisHtml
: '') +
8556 slotTime
.add(this.slotDuration
);
8557 slotIterator
.add(this.slotDuration
);
8565 ------------------------------------------------------------------------------------------------------------------*/
8568 // Parses various options into properties of this object
8569 processOptions: function() {
8570 var slotDuration
= this.opt('slotDuration');
8571 var snapDuration
= this.opt('snapDuration');
8574 slotDuration
= moment
.duration(slotDuration
);
8575 snapDuration
= snapDuration
? moment
.duration(snapDuration
) : slotDuration
;
8577 this.slotDuration
= slotDuration
;
8578 this.snapDuration
= snapDuration
;
8579 this.snapsPerSlot
= slotDuration
/ snapDuration
; // TODO: ensure an integer multiple?
8581 // might be an array value (for TimelineView).
8582 // if so, getting the most granular entry (the last one probably).
8583 input
= this.opt('slotLabelFormat');
8584 if ($.isArray(input
)) {
8585 input
= input
[input
.length
- 1];
8588 this.labelFormat
= input
||
8589 this.opt('smallTimeFormat'); // the computed default
8591 input
= this.opt('slotLabelInterval');
8592 this.labelInterval
= input
?
8593 moment
.duration(input
) :
8594 this.computeLabelInterval(slotDuration
);
8598 // Computes an automatic value for slotLabelInterval
8599 computeLabelInterval: function(slotDuration
) {
8604 // find the smallest stock label interval that results in more than one slots-per-label
8605 for (i
= AGENDA_STOCK_SUB_DURATIONS
.length
- 1; i
>= 0; i
--) {
8606 labelInterval
= moment
.duration(AGENDA_STOCK_SUB_DURATIONS
[i
]);
8607 slotsPerLabel
= divideDurationByDuration(labelInterval
, slotDuration
);
8608 if (isInt(slotsPerLabel
) && slotsPerLabel
> 1) {
8609 return labelInterval
;
8613 return moment
.duration(slotDuration
); // fall back. clone
8617 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
8618 computeEventTimeFormat: function() {
8619 return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
8623 // Computes a default `displayEventEnd` value if one is not expliclty defined
8624 computeDisplayEventEnd: function() {
8630 ------------------------------------------------------------------------------------------------------------------*/
8633 prepareHits: function() {
8634 this.colCoordCache
.build();
8635 this.slatCoordCache
.build();
8639 releaseHits: function() {
8640 this.colCoordCache
.clear();
8641 // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
8645 queryHit: function(leftOffset
, topOffset
) {
8646 var snapsPerSlot
= this.snapsPerSlot
;
8647 var colCoordCache
= this.colCoordCache
;
8648 var slatCoordCache
= this.slatCoordCache
;
8650 if (colCoordCache
.isLeftInBounds(leftOffset
) && slatCoordCache
.isTopInBounds(topOffset
)) {
8651 var colIndex
= colCoordCache
.getHorizontalIndex(leftOffset
);
8652 var slatIndex
= slatCoordCache
.getVerticalIndex(topOffset
);
8654 if (colIndex
!= null && slatIndex
!= null) {
8655 var slatTop
= slatCoordCache
.getTopOffset(slatIndex
);
8656 var slatHeight
= slatCoordCache
.getHeight(slatIndex
);
8657 var partial
= (topOffset
- slatTop
) / slatHeight
; // floating point number between 0 and 1
8658 var localSnapIndex
= Math
.floor(partial
* snapsPerSlot
); // the snap # relative to start of slat
8659 var snapIndex
= slatIndex
* snapsPerSlot
+ localSnapIndex
;
8660 var snapTop
= slatTop
+ (localSnapIndex
/ snapsPerSlot
) * slatHeight
;
8661 var snapBottom
= slatTop
+ ((localSnapIndex
+ 1) / snapsPerSlot
) * slatHeight
;
8666 component
: this, // needed unfortunately :(
8667 left
: colCoordCache
.getLeftOffset(colIndex
),
8668 right
: colCoordCache
.getRightOffset(colIndex
),
8677 getHitFootprint: function(hit
) {
8678 var start
= this.getCellDate(0, hit
.col
); // row=0
8679 var time
= this.computeSnapTime(hit
.snap
); // pass in the snap-index
8683 end
= start
.clone().add(this.snapDuration
);
8685 return new ComponentFootprint(
8686 new UnzonedRange(start
, end
),
8692 getHitEl: function(hit
) {
8693 return this.colEls
.eq(hit
.col
);
8698 ------------------------------------------------------------------------------------------------------------------*/
8701 rangeUpdated: function() {
8702 var view
= this.view
;
8704 this.updateDayTable();
8706 this.dayRanges
= this.dayDates
.map(function(dayDate
) {
8707 return new UnzonedRange(
8708 dayDate
.clone().add(view
.minTime
),
8709 dayDate
.clone().add(view
.maxTime
)
8715 // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
8716 computeSnapTime: function(snapIndex
) {
8717 return moment
.duration(this.view
.minTime
+ this.snapDuration
* snapIndex
);
8721 // Slices up the given span (unzoned start/end with other misc data) into an array of segments
8722 componentFootprintToSegs: function(componentFootprint
) {
8723 var segs
= this.sliceRangeByTimes(componentFootprint
.unzonedRange
);
8726 for (i
= 0; i
< segs
.length
; i
++) {
8728 segs
[i
].col
= this.daysPerRow
- 1 - segs
[i
].dayIndex
;
8731 segs
[i
].col
= segs
[i
].dayIndex
;
8739 sliceRangeByTimes: function(unzonedRange
) {
8744 for (dayIndex
= 0; dayIndex
< this.daysPerRow
; dayIndex
++) {
8746 segRange
= unzonedRange
.intersect(this.dayRanges
[dayIndex
]);
8750 startMs
: segRange
.startMs
,
8751 endMs
: segRange
.endMs
,
8752 isStart
: segRange
.isStart
,
8753 isEnd
: segRange
.isEnd
,
8764 ------------------------------------------------------------------------------------------------------------------*/
8767 updateSize: function(isResize
) { // NOT a standard Grid method
8768 this.slatCoordCache
.build();
8771 this.updateSegVerticals(
8772 [].concat(this.fgSegs
|| [], this.bgSegs
|| [], this.businessSegs
|| [])
8778 getTotalSlatHeight: function() {
8779 return this.slatContainerEl
.outerHeight();
8783 // Computes the top coordinate, relative to the bounds of the grid, of the given date.
8784 // `ms` can be a millisecond UTC time OR a UTC moment.
8785 // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
8786 computeDateTop: function(ms
, startOfDayDate
) {
8787 return this.computeTimeTop(
8789 ms
- startOfDayDate
.clone().stripTime()
8795 // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
8796 computeTimeTop: function(time
) {
8797 var len
= this.slatEls
.length
;
8798 var slatCoverage
= (time
- this.view
.minTime
) / this.slotDuration
; // floating-point value of # of slots covered
8802 // compute a floating-point number for how many slats should be progressed through.
8803 // from 0 to number of slats (inclusive)
8804 // constrained because minTime/maxTime might be customized.
8805 slatCoverage
= Math
.max(0, slatCoverage
);
8806 slatCoverage
= Math
.min(len
, slatCoverage
);
8808 // an integer index of the furthest whole slat
8809 // from 0 to number slats (*exclusive*, so len-1)
8810 slatIndex
= Math
.floor(slatCoverage
);
8811 slatIndex
= Math
.min(slatIndex
, len
- 1);
8813 // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
8814 // could be 1.0 if slatCoverage is covering *all* the slots
8815 slatRemainder
= slatCoverage
- slatIndex
;
8817 return this.slatCoordCache
.getTopPosition(slatIndex
) +
8818 this.slatCoordCache
.getHeight(slatIndex
) * slatRemainder
;
8823 /* Event Drag Visualization
8824 ------------------------------------------------------------------------------------------------------------------*/
8827 // Renders a visual indication of an event being dragged over the specified date(s).
8828 // A returned value of `true` signals that a mock "helper" event has been rendered.
8829 renderDrag: function(eventFootprints
, seg
) {
8832 if (seg
) { // if there is event information for this drag, render a helper event
8834 // returns mock event elements
8835 // signal that a helper has been rendered
8836 return this.renderHelperEventFootprints(eventFootprints
);
8838 else { // otherwise, just render a highlight
8840 for (i
= 0; i
< eventFootprints
.length
; i
++) {
8841 this.renderHighlight(eventFootprints
[i
].componentFootprint
);
8847 // Unrenders any visual indication of an event being dragged
8848 unrenderDrag: function() {
8849 this.unrenderHelper();
8850 this.unrenderHighlight();
8854 /* Event Resize Visualization
8855 ------------------------------------------------------------------------------------------------------------------*/
8858 // Renders a visual indication of an event being resized
8859 renderEventResize: function(eventFootprints
, seg
) {
8860 return this.renderHelperEventFootprints(eventFootprints
, seg
); // returns mock event elements
8864 // Unrenders any visual indication of an event being resized
8865 unrenderEventResize: function() {
8866 this.unrenderHelper();
8871 ------------------------------------------------------------------------------------------------------------------*/
8874 // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
8875 renderHelperEventFootprintEls: function(eventFootprints
, sourceSeg
) {
8876 var segs
= this.eventFootprintsToSegs(eventFootprints
);
8878 return this.renderHelperSegs( // returns mock event elements
8885 // Unrenders any mock helper event
8886 unrenderHelper: function() {
8887 this.unrenderHelperSegs();
8892 ------------------------------------------------------------------------------------------------------------------*/
8895 renderBusinessHours: function() {
8896 this.renderBusinessSegs(
8897 this.buildBusinessHourSegs()
8902 unrenderBusinessHours: function() {
8903 this.unrenderBusinessSegs();
8908 ------------------------------------------------------------------------------------------------------------------*/
8911 getNowIndicatorUnit: function() {
8912 return 'minute'; // will refresh on the minute
8916 renderNowIndicator: function(date
) {
8917 // seg system might be overkill, but it handles scenario where line needs to be rendered
8918 // more than once because of columns with the same date (resources columns for example)
8919 var segs
= this.componentFootprintToSegs(
8920 new ComponentFootprint(
8921 new UnzonedRange(date
, date
.valueOf() + 1), // protect against null range
8925 var top
= this.computeDateTop(date
, date
);
8929 // render lines within the columns
8930 for (i
= 0; i
< segs
.length
; i
++) {
8931 nodes
.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
8933 .appendTo(this.colContainerEls
.eq(segs
[i
].col
))[0]);
8936 // render an arrow over the axis
8937 if (segs
.length
> 0) { // is the current time in view?
8938 nodes
.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
8940 .appendTo(this.el
.find('.fc-content-skeleton'))[0]);
8943 this.nowIndicatorEls
= $(nodes
);
8947 unrenderNowIndicator: function() {
8948 if (this.nowIndicatorEls
) {
8949 this.nowIndicatorEls
.remove();
8950 this.nowIndicatorEls
= null;
8956 ------------------------------------------------------------------------------------------------------------------*/
8959 // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
8960 renderSelectionFootprint: function(componentFootprint
) {
8961 if (this.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
8962 this.renderHelperEventFootprints([
8963 this.fabricateEventFootprint(componentFootprint
)
8967 this.renderHighlight(componentFootprint
);
8972 // Unrenders any visual indication of a selection
8973 unrenderSelection: function() {
8974 this.unrenderHelper();
8975 this.unrenderHighlight();
8980 ------------------------------------------------------------------------------------------------------------------*/
8983 renderHighlight: function(componentFootprint
) {
8984 this.renderHighlightSegs(
8985 this.componentFootprintToSegs(componentFootprint
)
8990 unrenderHighlight: function() {
8991 this.unrenderHighlightSegs();
8998 /* Methods for rendering SEGMENTS, pieces of content that live on the view
8999 ( this file is no longer just for events )
9000 ----------------------------------------------------------------------------------------------------------------------*/
9004 colContainerEls
: null, // containers for each column
9006 // inner-containers for each column where different types of segs live
9007 fgContainerEls
: null,
9008 bgContainerEls
: null,
9009 helperContainerEls
: null,
9010 highlightContainerEls
: null,
9011 businessContainerEls
: null,
9013 // arrays of different types of displayed segments
9017 highlightSegs
: null,
9021 // Renders the DOM that the view's content will live in
9022 renderContentSkeleton: function() {
9027 for (i
= 0; i
< this.colCnt
; i
++) {
9030 '<div class="fc-content-col">' +
9031 '<div class="fc-event-container fc-helper-container"></div>' +
9032 '<div class="fc-event-container"></div>' +
9033 '<div class="fc-highlight-container"></div>' +
9034 '<div class="fc-bgevent-container"></div>' +
9035 '<div class="fc-business-container"></div>' +
9041 '<div class="fc-content-skeleton">' +
9043 '<tr>' + cellHtml
+ '</tr>' +
9048 this.colContainerEls
= skeletonEl
.find('.fc-content-col');
9049 this.helperContainerEls
= skeletonEl
.find('.fc-helper-container');
9050 this.fgContainerEls
= skeletonEl
.find('.fc-event-container:not(.fc-helper-container)');
9051 this.bgContainerEls
= skeletonEl
.find('.fc-bgevent-container');
9052 this.highlightContainerEls
= skeletonEl
.find('.fc-highlight-container');
9053 this.businessContainerEls
= skeletonEl
.find('.fc-business-container');
9055 this.bookendCells(skeletonEl
.find('tr')); // TODO: do this on string level
9056 this.el
.append(skeletonEl
);
9060 /* Foreground Events
9061 ------------------------------------------------------------------------------------------------------------------*/
9064 renderFgSegs: function(segs
) {
9065 segs
= this.renderFgSegsIntoContainers(segs
, this.fgContainerEls
);
9067 return segs
; // needed for Grid::renderEvents
9071 unrenderFgSegs: function() {
9072 this.unrenderNamedSegs('fgSegs');
9076 /* Foreground Helper Events
9077 ------------------------------------------------------------------------------------------------------------------*/
9080 renderHelperSegs: function(segs
, sourceSeg
) {
9085 segs
= this.renderFgSegsIntoContainers(segs
, this.helperContainerEls
);
9087 // Try to make the segment that is in the same row as sourceSeg look the same
9088 for (i
= 0; i
< segs
.length
; i
++) {
9090 if (sourceSeg
&& sourceSeg
.col
=== seg
.col
) {
9091 sourceEl
= sourceSeg
.el
;
9093 left
: sourceEl
.css('left'),
9094 right
: sourceEl
.css('right'),
9095 'margin-left': sourceEl
.css('margin-left'),
9096 'margin-right': sourceEl
.css('margin-right')
9099 helperEls
.push(seg
.el
[0]);
9102 this.helperSegs
= segs
;
9104 return $(helperEls
); // must return rendered helpers
9108 unrenderHelperSegs: function() {
9109 this.unrenderNamedSegs('helperSegs');
9113 /* Background Events
9114 ------------------------------------------------------------------------------------------------------------------*/
9117 renderBgSegs: function(segs
) {
9118 segs
= this.renderFillSegEls('bgEvent', segs
); // TODO: old fill system
9119 this.updateSegVerticals(segs
);
9120 this.attachSegsByCol(this.groupSegsByCol(segs
), this.bgContainerEls
);
9122 return segs
; // needed for Grid::renderEvents
9126 unrenderBgSegs: function() {
9127 this.unrenderNamedSegs('bgSegs');
9132 ------------------------------------------------------------------------------------------------------------------*/
9135 renderHighlightSegs: function(segs
) {
9136 segs
= this.renderFillSegEls('highlight', segs
); // TODO: instead of calling renderFill directly
9137 this.updateSegVerticals(segs
);
9138 this.attachSegsByCol(this.groupSegsByCol(segs
), this.highlightContainerEls
);
9139 this.highlightSegs
= segs
;
9143 unrenderHighlightSegs: function() {
9144 this.unrenderNamedSegs('highlightSegs');
9149 ------------------------------------------------------------------------------------------------------------------*/
9152 renderBusinessSegs: function(segs
) {
9153 segs
= this.renderFillSegEls('businessHours', segs
); // TODO: instead of calling renderFill directly
9154 this.updateSegVerticals(segs
);
9155 this.attachSegsByCol(this.groupSegsByCol(segs
), this.businessContainerEls
);
9156 this.businessSegs
= segs
;
9160 unrenderBusinessSegs: function() {
9161 this.unrenderNamedSegs('businessSegs');
9165 /* Seg Rendering Utils
9166 ------------------------------------------------------------------------------------------------------------------*/
9169 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
9170 groupSegsByCol: function(segs
) {
9174 for (i
= 0; i
< this.colCnt
; i
++) {
9178 for (i
= 0; i
< segs
.length
; i
++) {
9179 segsByCol
[segs
[i
].col
].push(segs
[i
]);
9186 // Given segments grouped by column, insert the segments' elements into a parallel array of container
9187 // elements, each living within a column.
9188 attachSegsByCol: function(segsByCol
, containerEls
) {
9193 for (col
= 0; col
< this.colCnt
; col
++) { // iterate each column grouping
9194 segs
= segsByCol
[col
];
9196 for (i
= 0; i
< segs
.length
; i
++) {
9197 containerEls
.eq(col
).append(segs
[i
].el
);
9203 // Given the name of a property of `this` object, assumed to be an array of segments,
9204 // loops through each segment and removes from DOM. Will null-out the property afterwards.
9205 unrenderNamedSegs: function(propName
) {
9206 var segs
= this[propName
];
9210 for (i
= 0; i
< segs
.length
; i
++) {
9211 segs
[i
].el
.remove();
9213 this[propName
] = null;
9219 /* Foreground Event Rendering Utils
9220 ------------------------------------------------------------------------------------------------------------------*/
9223 // Given an array of foreground segments, render a DOM element for each, computes position,
9224 // and attaches to the column inner-container elements.
9225 renderFgSegsIntoContainers: function(segs
, containerEls
) {
9229 segs
= this.renderFgSegEls(segs
); // will call fgSegHtml
9230 segsByCol
= this.groupSegsByCol(segs
);
9232 for (col
= 0; col
< this.colCnt
; col
++) {
9233 this.updateFgSegCoords(segsByCol
[col
]);
9236 this.attachSegsByCol(segsByCol
, containerEls
);
9242 // Renders the HTML for a single event segment's default rendering
9243 fgSegHtml: function(seg
, disableResizing
) {
9244 var view
= this.view
;
9245 var calendar
= view
.calendar
;
9246 var componentFootprint
= seg
.footprint
.componentFootprint
;
9247 var isAllDay
= componentFootprint
.isAllDay
;
9248 var eventDef
= seg
.footprint
.eventDef
;
9249 var isDraggable
= view
.isEventDefDraggable(eventDef
);
9250 var isResizableFromStart
= !disableResizing
&& seg
.isStart
&& view
.isEventDefResizableFromStart(eventDef
);
9251 var isResizableFromEnd
= !disableResizing
&& seg
.isEnd
&& view
.isEventDefResizableFromEnd(eventDef
);
9252 var classes
= this.getSegClasses(seg
, isDraggable
, isResizableFromStart
|| isResizableFromEnd
);
9253 var skinCss
= cssToStr(this.getSegSkinCss(seg
));
9255 var fullTimeText
; // more verbose time text. for the print stylesheet
9256 var startTimeText
; // just the start time text
9258 classes
.unshift('fc-time-grid-event', 'fc-v-event');
9260 // if the event appears to span more than one day...
9261 if (view
.isMultiDayRange(componentFootprint
.unzonedRange
)) {
9262 // Don't display time text on segments that run entirely through a day.
9263 // That would appear as midnight-midnight and would look dumb.
9264 // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
9265 if (seg
.isStart
|| seg
.isEnd
) {
9266 var zonedStart
= calendar
.msToMoment(seg
.startMs
);
9267 var zonedEnd
= calendar
.msToMoment(seg
.endMs
);
9268 timeText
= this._getEventTimeText(zonedStart
, zonedEnd
, isAllDay
);
9269 fullTimeText
= this._getEventTimeText(zonedStart
, zonedEnd
, isAllDay
, 'LT');
9270 startTimeText
= this._getEventTimeText(zonedStart
, zonedEnd
, isAllDay
, null, false); // displayEnd=false
9274 // Display the normal time text for the *event's* times
9275 timeText
= this.getEventTimeText(seg
.footprint
);
9276 fullTimeText
= this.getEventTimeText(seg
.footprint
, 'LT');
9277 startTimeText
= this.getEventTimeText(seg
.footprint
, null, false); // displayEnd=false
9280 return '<a class="' + classes
.join(' ') + '"' +
9282 ' href="' + htmlEscape(eventDef
.url
) + '"' :
9286 ' style="' + skinCss
+ '"' :
9290 '<div class="fc-content">' +
9292 '<div class="fc-time"' +
9293 ' data-start="' + htmlEscape(startTimeText
) + '"' +
9294 ' data-full="' + htmlEscape(fullTimeText
) + '"' +
9296 '<span>' + htmlEscape(timeText
) + '</span>' +
9301 '<div class="fc-title">' +
9302 htmlEscape(eventDef
.title
) +
9307 '<div class="fc-bg"/>' +
9308 /* TODO: write CSS for this
9309 (isResizableFromStart ?
9310 '<div class="fc-resizer fc-start-resizer" />' :
9314 (isResizableFromEnd
?
9315 '<div class="fc-resizer fc-end-resizer" />' :
9322 /* Seg Position Utils
9323 ------------------------------------------------------------------------------------------------------------------*/
9326 // Refreshes the CSS top/bottom coordinates for each segment element.
9327 // Works when called after initial render, after a window resize/zoom for example.
9328 updateSegVerticals: function(segs
) {
9329 this.computeSegVerticals(segs
);
9330 this.assignSegVerticals(segs
);
9334 // For each segment in an array, computes and assigns its top and bottom properties
9335 computeSegVerticals: function(segs
) {
9339 for (i
= 0; i
< segs
.length
; i
++) {
9341 dayDate
= this.dayDates
[seg
.dayIndex
];
9343 seg
.top
= this.computeDateTop(seg
.startMs
, dayDate
);
9344 seg
.bottom
= this.computeDateTop(seg
.endMs
, dayDate
);
9349 // Given segments that already have their top/bottom properties computed, applies those values to
9350 // the segments' elements.
9351 assignSegVerticals: function(segs
) {
9354 for (i
= 0; i
< segs
.length
; i
++) {
9356 seg
.el
.css(this.generateSegVerticalCss(seg
));
9361 // Generates an object with CSS properties for the top/bottom coordinates of a segment element
9362 generateSegVerticalCss: function(seg
) {
9365 bottom
: -seg
.bottom
// flipped because needs to be space beyond bottom edge of event container
9370 /* Foreground Event Positioning Utils
9371 ------------------------------------------------------------------------------------------------------------------*/
9374 // Given segments that are assumed to all live in the *same column*,
9375 // compute their verical/horizontal coordinates and assign to their elements.
9376 updateFgSegCoords: function(segs
) {
9377 this.computeSegVerticals(segs
); // horizontals relies on this
9378 this.computeFgSegHorizontals(segs
); // compute horizontal coordinates, z-index's, and reorder the array
9379 this.assignSegVerticals(segs
);
9380 this.assignFgSegHorizontals(segs
);
9384 // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
9385 // NOTE: Also reorders the given array by date!
9386 computeFgSegHorizontals: function(segs
) {
9391 this.sortEventSegs(segs
); // order by certain criteria
9392 levels
= buildSlotSegLevels(segs
);
9393 computeForwardSlotSegs(levels
);
9395 if ((level0
= levels
[0])) {
9397 for (i
= 0; i
< level0
.length
; i
++) {
9398 computeSlotSegPressures(level0
[i
]);
9401 for (i
= 0; i
< level0
.length
; i
++) {
9402 this.computeFgSegForwardBack(level0
[i
], 0, 0);
9408 // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
9409 // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
9410 // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
9412 // The segment might be part of a "series", which means consecutive segments with the same pressure
9413 // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
9414 // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
9415 // coordinate of the first segment in the series.
9416 computeFgSegForwardBack: function(seg
, seriesBackwardPressure
, seriesBackwardCoord
) {
9417 var forwardSegs
= seg
.forwardSegs
;
9420 if (seg
.forwardCoord
=== undefined) { // not already computed
9422 if (!forwardSegs
.length
) {
9424 // if there are no forward segments, this segment should butt up against the edge
9425 seg
.forwardCoord
= 1;
9429 // sort highest pressure first
9430 this.sortForwardSegs(forwardSegs
);
9432 // this segment's forwardCoord will be calculated from the backwardCoord of the
9433 // highest-pressure forward segment.
9434 this.computeFgSegForwardBack(forwardSegs
[0], seriesBackwardPressure
+ 1, seriesBackwardCoord
);
9435 seg
.forwardCoord
= forwardSegs
[0].backwardCoord
;
9438 // calculate the backwardCoord from the forwardCoord. consider the series
9439 seg
.backwardCoord
= seg
.forwardCoord
-
9440 (seg
.forwardCoord
- seriesBackwardCoord
) / // available width for series
9441 (seriesBackwardPressure
+ 1); // # of segments in the series
9443 // use this segment's coordinates to computed the coordinates of the less-pressurized
9445 for (i
=0; i
<forwardSegs
.length
; i
++) {
9446 this.computeFgSegForwardBack(forwardSegs
[i
], 0, seg
.forwardCoord
);
9452 sortForwardSegs: function(forwardSegs
) {
9453 forwardSegs
.sort(proxy(this, 'compareForwardSegs'));
9457 // A cmp function for determining which forward segment to rely on more when computing coordinates.
9458 compareForwardSegs: function(seg1
, seg2
) {
9459 // put higher-pressure first
9460 return seg2
.forwardPressure
- seg1
.forwardPressure
||
9461 // put segments that are closer to initial edge first (and favor ones with no coords yet)
9462 (seg1
.backwardCoord
|| 0) - (seg2
.backwardCoord
|| 0) ||
9463 // do normal sorting...
9464 this.compareEventSegs(seg1
, seg2
);
9468 // Given foreground event segments that have already had their position coordinates computed,
9469 // assigns position-related CSS values to their elements.
9470 assignFgSegHorizontals: function(segs
) {
9473 for (i
= 0; i
< segs
.length
; i
++) {
9475 seg
.el
.css(this.generateFgSegHorizontalCss(seg
));
9477 // if the height is short, add a className for alternate styling
9478 if (seg
.bottom
- seg
.top
< 30) {
9479 seg
.el
.addClass('fc-short');
9485 // Generates an object with CSS properties/values that should be applied to an event segment element.
9486 // Contains important positioning-related properties that should be applied to any event element, customized or not.
9487 generateFgSegHorizontalCss: function(seg
) {
9488 var shouldOverlap
= this.opt('slotEventOverlap');
9489 var backwardCoord
= seg
.backwardCoord
; // the left side if LTR. the right side if RTL. floating-point
9490 var forwardCoord
= seg
.forwardCoord
; // the right side if LTR. the left side if RTL. floating-point
9491 var props
= this.generateSegVerticalCss(seg
); // get top/bottom first
9492 var left
; // amount of space from left edge, a fraction of the total width
9493 var right
; // amount of space from right edge, a fraction of the total width
9495 if (shouldOverlap
) {
9496 // double the width, but don't go beyond the maximum forward coordinate (1.0)
9497 forwardCoord
= Math
.min(1, backwardCoord
+ (forwardCoord
- backwardCoord
) * 2);
9501 left
= 1 - forwardCoord
;
9502 right
= backwardCoord
;
9505 left
= backwardCoord
;
9506 right
= 1 - forwardCoord
;
9509 props
.zIndex
= seg
.level
+ 1; // convert from 0-base to 1-based
9510 props
.left
= left
* 100 + '%';
9511 props
.right
= right
* 100 + '%';
9513 if (shouldOverlap
&& seg
.forwardPressure
) {
9514 // add padding to the edge so that forward stacked events don't cover the resizer's icon
9515 props
[this.isRTL
? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
9524 // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
9525 // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
9526 function buildSlotSegLevels(segs
) {
9531 for (i
=0; i
<segs
.length
; i
++) {
9534 // go through all the levels and stop on the first level where there are no collisions
9535 for (j
=0; j
<levels
.length
; j
++) {
9536 if (!computeSlotSegCollisions(seg
, levels
[j
]).length
) {
9543 (levels
[j
] || (levels
[j
] = [])).push(seg
);
9550 // For every segment, figure out the other segments that are in subsequent
9551 // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
9552 function computeForwardSlotSegs(levels
) {
9557 for (i
=0; i
<levels
.length
; i
++) {
9560 for (j
=0; j
<level
.length
; j
++) {
9563 seg
.forwardSegs
= [];
9564 for (k
=i
+1; k
<levels
.length
; k
++) {
9565 computeSlotSegCollisions(seg
, levels
[k
], seg
.forwardSegs
);
9572 // Figure out which path forward (via seg.forwardSegs) results in the longest path until
9573 // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
9574 function computeSlotSegPressures(seg
) {
9575 var forwardSegs
= seg
.forwardSegs
;
9576 var forwardPressure
= 0;
9579 if (seg
.forwardPressure
=== undefined) { // not already computed
9581 for (i
=0; i
<forwardSegs
.length
; i
++) {
9582 forwardSeg
= forwardSegs
[i
];
9584 // figure out the child's maximum forward path
9585 computeSlotSegPressures(forwardSeg
);
9587 // either use the existing maximum, or use the child's forward pressure
9588 // plus one (for the forwardSeg itself)
9589 forwardPressure
= Math
.max(
9591 1 + forwardSeg
.forwardPressure
9595 seg
.forwardPressure
= forwardPressure
;
9600 // Find all the segments in `otherSegs` that vertically collide with `seg`.
9601 // Append into an optionally-supplied `results` array and return.
9602 function computeSlotSegCollisions(seg
, otherSegs
, results
) {
9603 results
= results
|| [];
9605 for (var i
=0; i
<otherSegs
.length
; i
++) {
9606 if (isSlotSegCollision(seg
, otherSegs
[i
])) {
9607 results
.push(otherSegs
[i
]);
9615 // Do these segments occupy the same vertical space?
9616 function isSlotSegCollision(seg1
, seg2
) {
9617 return seg1
.bottom
> seg2
.top
&& seg1
.top
< seg2
.bottom
;
9622 /* An abstract class from which other views inherit from
9623 ----------------------------------------------------------------------------------------------------------------------*/
9625 var View
= FC
.View
= ChronoComponent
.extend({
9627 type
: null, // subclass' view name (string)
9628 name
: null, // deprecated. use `type` instead
9629 title
: null, // the text that will be displayed in the header's title
9631 calendar
: null, // owner Calendar object
9633 options
: null, // hash containing all options. already merged with view-specific-options
9636 batchRenderDepth
: 0,
9637 isDatesRendered
: false,
9638 isEventsRendered
: false,
9639 isBaseRendered
: false, // related to viewRender/viewDestroy triggers
9643 isSelected
: false, // boolean whether a range of time is user-selected or not
9644 selectedEventInstance
: null,
9646 eventOrderSpecs
: null, // criteria for ordering events when they have same date/time
9648 // for date utils, computed from options
9649 isHiddenDayHash
: null,
9652 isNowIndicatorRendered
: null,
9653 initialNowDate
: null, // result first getNow call
9654 initialNowQueriedMs
: null, // ms time the getNow was called
9655 nowIndicatorTimeoutID
: null, // for refresh timing of now indicator
9656 nowIndicatorIntervalID
: null, // "
9659 constructor: function(calendar
, viewSpec
) {
9660 this.calendar
= calendar
;
9661 this.viewSpec
= viewSpec
;
9664 this.type
= viewSpec
.type
;
9665 this.options
= viewSpec
.options
;
9667 // .name is deprecated
9668 this.name
= this.type
;
9670 ChronoComponent
.call(this);
9672 this.initHiddenDays();
9673 this.eventOrderSpecs
= parseFieldSpecs(this.opt('eventOrder'));
9675 this.renderQueue
= this.buildRenderQueue();
9676 this.initAutoBatchRender();
9682 buildRenderQueue: function() {
9684 var renderQueue
= new RenderQueue({
9685 event
: this.opt('eventRenderWait')
9688 renderQueue
.on('start', function() {
9689 _this
.freezeHeight();
9690 _this
.addScroll(_this
.queryScroll());
9693 renderQueue
.on('stop', function() {
9702 initAutoBatchRender: function() {
9705 this.on('before:change', function() {
9706 _this
.startBatchRender();
9709 this.on('change', function() {
9710 _this
.stopBatchRender();
9715 startBatchRender: function() {
9716 if (!(this.batchRenderDepth
++)) {
9717 this.renderQueue
.pause();
9722 stopBatchRender: function() {
9723 if (!(--this.batchRenderDepth
)) {
9724 this.renderQueue
.resume();
9729 // A good place for subclasses to initialize member variables
9730 initialize: function() {
9731 // subclasses can implement
9735 // Retrieves an option with the given name
9736 opt: function(name
) {
9737 return this.options
[name
];
9741 /* Title and Date Formatting
9742 ------------------------------------------------------------------------------------------------------------------*/
9745 // Computes what the title at the top of the calendar should be for this view
9746 computeTitle: function() {
9749 // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
9750 if (/^(year|month)$/.test(this.currentRangeUnit
)) {
9751 unzonedRange
= this.currentUnzonedRange
;
9753 else { // for day units or smaller, use the actual day range
9754 unzonedRange
= this.activeUnzonedRange
;
9757 return this.formatRange(
9759 start
: this.calendar
.msToMoment(unzonedRange
.startMs
, this.isRangeAllDay
),
9760 end
: this.calendar
.msToMoment(unzonedRange
.endMs
, this.isRangeAllDay
)
9763 this.opt('titleFormat') || this.computeTitleFormat(),
9764 this.opt('titleRangeSeparator')
9769 // Generates the format string that should be used to generate the title for the current date range.
9770 // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
9771 computeTitleFormat: function() {
9772 if (this.currentRangeUnit
== 'year') {
9775 else if (this.currentRangeUnit
== 'month') {
9776 return this.opt('monthYearFormat'); // like "September 2014"
9778 else if (this.currentRangeAs('days') > 1) {
9779 return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
9782 return 'LL'; // one day. longer, like "September 9 2014"
9788 // -----------------------------------------------------------------------------------------------------------------
9791 setElement: function(el
) {
9792 ChronoComponent
.prototype.setElement
.apply(this, arguments
);
9794 this.bindBaseRenderHandlers();
9798 removeElement: function() {
9800 this.unbindBaseRenderHandlers();
9802 ChronoComponent
.prototype.removeElement
.apply(this, arguments
);
9806 // Date Setting/Unsetting
9807 // -----------------------------------------------------------------------------------------------------------------
9810 setDate: function(date
) {
9811 var currentDateProfile
= this.get('dateProfile');
9812 var newDateProfile
= this.buildDateProfile(date
, null, true); // forceToValid=true
9815 !currentDateProfile
||
9816 !currentDateProfile
.activeUnzonedRange
.equals(newDateProfile
.activeUnzonedRange
)
9818 this.set('dateProfile', newDateProfile
);
9821 return newDateProfile
.date
;
9825 unsetDate: function() {
9826 this.unset('dateProfile');
9831 // -----------------------------------------------------------------------------------------------------------------
9834 requestDateRender: function(dateProfile
) {
9837 this.renderQueue
.queue(function() {
9838 _this
.executeDateRender(dateProfile
);
9843 requestDateUnrender: function() {
9846 this.renderQueue
.queue(function() {
9847 _this
.executeDateUnrender();
9848 }, 'date', 'destroy');
9853 // -----------------------------------------------------------------------------------------------------------------
9856 fetchInitialEvents: function(dateProfile
) {
9857 var calendar
= this.calendar
;
9858 var forceAllDay
= dateProfile
.isRangeAllDay
&& !this.usesMinMaxTime
;
9860 return calendar
.requestEvents(
9861 calendar
.msToMoment(dateProfile
.activeUnzonedRange
.startMs
, forceAllDay
),
9862 calendar
.msToMoment(dateProfile
.activeUnzonedRange
.endMs
, forceAllDay
)
9867 bindEventChanges: function() {
9868 this.listenTo(this.calendar
, 'eventsReset', this.resetEvents
);
9872 unbindEventChanges: function() {
9873 this.stopListeningTo(this.calendar
, 'eventsReset');
9877 setEvents: function(eventsPayload
) {
9878 this.set('currentEvents', eventsPayload
);
9879 this.set('hasEvents', true);
9883 unsetEvents: function() {
9884 this.unset('currentEvents');
9885 this.unset('hasEvents');
9889 resetEvents: function(eventsPayload
) {
9890 this.startBatchRender();
9892 this.setEvents(eventsPayload
);
9893 this.stopBatchRender();
9898 // -----------------------------------------------------------------------------------------------------------------
9901 requestEventsRender: function(eventsPayload
) {
9904 this.renderQueue
.queue(function() {
9905 _this
.executeEventsRender(eventsPayload
);
9906 }, 'event', 'init');
9910 requestEventsUnrender: function() {
9913 this.renderQueue
.queue(function() {
9914 _this
.executeEventsUnrender();
9915 }, 'event', 'destroy');
9919 // Date High-level Rendering
9920 // -----------------------------------------------------------------------------------------------------------------
9923 // if dateProfile not specified, uses current
9924 executeDateRender: function(dateProfile
, skipScroll
) {
9926 this.setDateProfileForRendering(dateProfile
);
9929 this.render(); // TODO: deprecate
9934 this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
9935 this.startNowIndicator();
9938 this.addScroll(this.computeInitialDateScroll());
9941 this.isDatesRendered
= true;
9942 this.trigger('datesRendered');
9946 executeDateUnrender: function() {
9949 this.stopNowIndicator();
9951 this.trigger('before:datesUnrendered');
9953 this.unrenderBusinessHours();
9954 this.unrenderDates();
9957 this.destroy(); // TODO: deprecate
9960 this.isDatesRendered
= false;
9964 // Determing when the "meat" of the view is rendered (aka the base)
9965 // -----------------------------------------------------------------------------------------------------------------
9968 bindBaseRenderHandlers: function() {
9971 this.on('datesRendered.baseHandler', function() {
9972 _this
.onBaseRender();
9975 this.on('before:datesUnrendered.baseHandler', function() {
9976 _this
.onBeforeBaseUnrender();
9981 unbindBaseRenderHandlers: function() {
9982 this.off('.baseHandler');
9986 onBaseRender: function() {
9987 this.applyScreenState();
9988 this.publiclyTrigger('viewRender', {
9990 args
: [ this, this.el
]
9995 onBeforeBaseUnrender: function() {
9996 this.applyScreenState();
9997 this.publiclyTrigger('viewDestroy', {
9999 args
: [ this, this.el
]
10004 // Misc view rendering utils
10005 // -----------------------------------------------------------------------------------------------------------------
10008 // Binds DOM handlers to elements that reside outside the view container, such as the document
10009 bindGlobalHandlers: function() {
10010 this.listenTo(GlobalEmitter
.get(), {
10011 touchstart
: this.processUnselect
,
10012 mousedown
: this.handleDocumentMousedown
10017 // Unbinds DOM handlers from elements that reside outside the view container
10018 unbindGlobalHandlers: function() {
10019 this.stopListeningTo(GlobalEmitter
.get());
10024 ------------------------------------------------------------------------------------------------------------------*/
10027 // Immediately render the current time indicator and begins re-rendering it at an interval,
10028 // which is defined by this.getNowIndicatorUnit().
10029 // TODO: somehow do this for the current whole day's background too
10030 startNowIndicator: function() {
10034 var delay
; // ms wait value
10036 if (this.opt('nowIndicator')) {
10037 unit
= this.getNowIndicatorUnit();
10039 update
= proxy(this, 'updateNowIndicator'); // bind to `this`
10041 this.initialNowDate
= this.calendar
.getNow();
10042 this.initialNowQueriedMs
= +new Date();
10043 this.renderNowIndicator(this.initialNowDate
);
10044 this.isNowIndicatorRendered
= true;
10046 // wait until the beginning of the next interval
10047 delay
= this.initialNowDate
.clone().startOf(unit
).add(1, unit
) - this.initialNowDate
;
10048 this.nowIndicatorTimeoutID
= setTimeout(function() {
10049 _this
.nowIndicatorTimeoutID
= null;
10051 delay
= +moment
.duration(1, unit
);
10052 delay
= Math
.max(100, delay
); // prevent too frequent
10053 _this
.nowIndicatorIntervalID
= setInterval(update
, delay
); // update every interval
10060 // rerenders the now indicator, computing the new current time from the amount of time that has passed
10061 // since the initial getNow call.
10062 updateNowIndicator: function() {
10063 if (this.isNowIndicatorRendered
) {
10064 this.unrenderNowIndicator();
10065 this.renderNowIndicator(
10066 this.initialNowDate
.clone().add(new Date() - this.initialNowQueriedMs
) // add ms
10072 // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
10073 // Won't cause side effects if indicator isn't rendered.
10074 stopNowIndicator: function() {
10075 if (this.isNowIndicatorRendered
) {
10077 if (this.nowIndicatorTimeoutID
) {
10078 clearTimeout(this.nowIndicatorTimeoutID
);
10079 this.nowIndicatorTimeoutID
= null;
10081 if (this.nowIndicatorIntervalID
) {
10082 clearTimeout(this.nowIndicatorIntervalID
);
10083 this.nowIndicatorIntervalID
= null;
10086 this.unrenderNowIndicator();
10087 this.isNowIndicatorRendered
= false;
10093 ------------------------------------------------------------------------------------------------------------------*/
10094 // TODO: move some of these to ChronoComponent
10097 // Refreshes anything dependant upon sizing of the container element of the grid
10098 updateSize: function(isResize
) {
10102 scroll
= this.queryScroll();
10105 this.updateHeight(isResize
);
10106 this.updateWidth(isResize
);
10107 this.updateNowIndicator();
10110 this.applyScroll(scroll
);
10115 // Refreshes the horizontal dimensions of the calendar
10116 updateWidth: function(isResize
) {
10117 // subclasses should implement
10121 // Refreshes the vertical dimensions of the calendar
10122 updateHeight: function(isResize
) {
10123 var calendar
= this.calendar
; // we poll the calendar for height information
10126 calendar
.getSuggestedViewHeight(),
10127 calendar
.isHeightAuto()
10132 // Updates the vertical dimensions of the calendar to the specified height.
10133 // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
10134 setHeight: function(height
, isAuto
) {
10135 // subclasses should implement
10140 ------------------------------------------------------------------------------------------------------------------*/
10143 addForcedScroll: function(scroll
) {
10145 $.extend(scroll
, { isForced
: true })
10150 addScroll: function(scroll
) {
10151 var queuedScroll
= this.queuedScroll
|| (this.queuedScroll
= {});
10153 if (!queuedScroll
.isForced
) {
10154 $.extend(queuedScroll
, scroll
);
10159 popScroll: function() {
10160 this.applyQueuedScroll();
10161 this.queuedScroll
= null;
10165 applyQueuedScroll: function() {
10166 if (this.queuedScroll
) {
10167 this.applyScroll(this.queuedScroll
);
10172 queryScroll: function() {
10175 if (this.isDatesRendered
) {
10176 $.extend(scroll
, this.queryDateScroll());
10183 applyScroll: function(scroll
) {
10184 if (this.isDatesRendered
) {
10185 this.applyDateScroll(scroll
);
10190 computeInitialDateScroll: function() {
10191 return {}; // subclasses must implement
10195 queryDateScroll: function() {
10196 return {}; // subclasses must implement
10200 applyDateScroll: function(scroll
) {
10201 ; // subclasses must implement
10206 ------------------------------------------------------------------------------------------------------------------*/
10209 freezeHeight: function() {
10210 this.calendar
.freezeContentHeight();
10214 thawHeight: function() {
10215 this.calendar
.thawContentHeight();
10219 // Event High-level Rendering
10220 // -----------------------------------------------------------------------------------------------------------------
10223 executeEventsRender: function(eventsPayload
) {
10225 if (this.renderEvents
) { // for legacy custom views
10226 this.renderEvents(convertEventsPayloadToLegacyArray(eventsPayload
));
10229 this.renderEventsPayload(eventsPayload
);
10232 this.isEventsRendered
= true;
10234 this.onEventsRender();
10238 executeEventsUnrender: function() {
10239 this.onBeforeEventsUnrender();
10241 if (this.destroyEvents
) {
10242 this.destroyEvents(); // TODO: deprecate
10245 this.unrenderEvents();
10246 this.isEventsRendered
= false;
10250 // Event Rendering Triggers
10251 // -----------------------------------------------------------------------------------------------------------------
10254 // Signals that all events have been rendered
10255 onEventsRender: function() {
10257 var hasSingleHandlers
= this.hasPublicHandlers('eventAfterRender');
10259 if (hasSingleHandlers
|| this.hasPublicHandlers('eventAfterAllRender')) {
10260 this.applyScreenState();
10263 if (hasSingleHandlers
) {
10264 this.getEventSegs().forEach(function(seg
) {
10267 if (seg
.el
) { // necessary?
10268 legacy
= seg
.footprint
.getEventLegacy();
10270 _this
.publiclyTrigger('eventAfterRender', {
10272 args
: [ legacy
, seg
.el
, _this
]
10278 this.publiclyTrigger('eventAfterAllRender', {
10285 // Signals that all event elements are about to be removed
10286 onBeforeEventsUnrender: function() {
10289 if (this.hasPublicHandlers('eventDestroy')) {
10291 this.applyScreenState();
10293 this.getEventSegs().forEach(function(seg
) {
10296 if (seg
.el
) { // necessary?
10297 legacy
= seg
.footprint
.getEventLegacy();
10299 _this
.publiclyTrigger('eventDestroy', {
10301 args
: [ legacy
, seg
.el
, _this
]
10309 applyScreenState: function() {
10311 this.freezeHeight();
10312 this.applyQueuedScroll();
10316 // Event Rendering Utils
10317 // -----------------------------------------------------------------------------------------------------------------
10318 // TODO: move this to ChronoComponent
10321 // Hides all rendered event segments linked to the given event
10322 showEventsWithId: function(eventDefId
) {
10323 this.getEventSegs().forEach(function(seg
) {
10325 seg
.footprint
.eventDef
.id
=== eventDefId
&&
10326 seg
.el
// necessary?
10328 seg
.el
.css('visibility', '');
10334 // Shows all rendered event segments linked to the given event
10335 hideEventsWithId: function(eventDefId
) {
10336 this.getEventSegs().forEach(function(seg
) {
10338 seg
.footprint
.eventDef
.id
=== eventDefId
&&
10339 seg
.el
// necessary?
10341 seg
.el
.css('visibility', 'hidden');
10347 /* Event Drag-n-Drop
10348 ------------------------------------------------------------------------------------------------------------------*/
10351 reportEventDrop: function(eventInstance
, eventMutation
, el
, ev
) {
10352 var eventManager
= this.calendar
.eventManager
;
10353 var undoFunc
= eventManager
.mutateEventsWithId(
10354 eventInstance
.def
.id
,
10358 var dateMutation
= eventMutation
.dateMutation
;
10360 // update the EventInstance, for handlers
10361 if (dateMutation
) {
10362 eventInstance
.dateProfile
= dateMutation
.buildNewDateProfile(
10363 eventInstance
.dateProfile
,
10368 this.triggerEventDrop(
10370 // a drop doesn't necessarily mean a date mutation (ex: resource change)
10371 (dateMutation
&& dateMutation
.dateDelta
) || moment
.duration(),
10378 // Triggers event-drop handlers that have subscribed via the API
10379 triggerEventDrop: function(eventInstance
, dateDelta
, undoFunc
, el
, ev
) {
10380 this.publiclyTrigger('eventDrop', {
10383 eventInstance
.toLegacy(),
10387 {}, // {} = jqui dummy
10394 /* External Element Drag-n-Drop
10395 ------------------------------------------------------------------------------------------------------------------*/
10398 // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
10399 // `meta` is the parsed data that has been embedded into the dragging event.
10400 // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
10401 reportExternalDrop: function(singleEventDef
, isEvent
, isSticky
, el
, ev
, ui
) {
10404 this.calendar
.eventManager
.addEventDef(singleEventDef
, isSticky
);
10407 this.triggerExternalDrop(singleEventDef
, isEvent
, el
, ev
, ui
);
10411 // Triggers external-drop handlers that have subscribed via the API
10412 triggerExternalDrop: function(singleEventDef
, isEvent
, el
, ev
, ui
) {
10414 // trigger 'drop' regardless of whether element represents an event
10415 this.publiclyTrigger('drop', {
10418 singleEventDef
.dateProfile
.start
.clone(),
10426 // signal an external event landed
10427 this.publiclyTrigger('eventReceive', {
10430 singleEventDef
.buildInstance().toLegacy(),
10439 ------------------------------------------------------------------------------------------------------------------*/
10442 // Must be called when an event in the view has been resized to a new length
10443 reportEventResize: function(eventInstance
, eventMutation
, el
, ev
) {
10444 var eventManager
= this.calendar
.eventManager
;
10445 var undoFunc
= eventManager
.mutateEventsWithId(
10446 eventInstance
.def
.id
,
10451 // update the EventInstance, for handlers
10452 eventInstance
.dateProfile
= eventMutation
.dateMutation
.buildNewDateProfile(
10453 eventInstance
.dateProfile
,
10457 this.triggerEventResize(
10459 eventMutation
.dateMutation
.endDelta
,
10466 // Triggers event-resize handlers that have subscribed via the API
10467 triggerEventResize: function(eventInstance
, durationDelta
, undoFunc
, el
, ev
) {
10468 this.publiclyTrigger('eventResize', {
10471 eventInstance
.toLegacy(),
10475 {}, // {} = jqui dummy
10482 /* Selection (time range)
10483 ------------------------------------------------------------------------------------------------------------------*/
10486 // Selects a date span on the view. `start` and `end` are both Moments.
10487 // `ev` is the native mouse event that begin the interaction.
10488 select: function(footprint
, ev
) {
10490 this.renderSelectionFootprint(footprint
);
10491 this.reportSelection(footprint
, ev
);
10495 renderSelectionFootprint: function(footprint
, ev
) {
10496 if (this.renderSelection
) { // legacy method in custom view classes
10497 this.renderSelection(
10498 footprint
.toLegacy(this.calendar
)
10502 ChronoComponent
.prototype.renderSelectionFootprint
.apply(this, arguments
);
10507 // Called when a new selection is made. Updates internal state and triggers handlers.
10508 reportSelection: function(footprint
, ev
) {
10509 this.isSelected
= true;
10510 this.triggerSelect(footprint
, ev
);
10514 // Triggers handlers to 'select'
10515 triggerSelect: function(footprint
, ev
) {
10516 var dateProfile
= this.calendar
.footprintToDateProfile(footprint
); // abuse of "Event"DateProfile?
10518 this.publiclyTrigger('select', {
10530 // Undoes a selection. updates in the internal state and triggers handlers.
10531 // `ev` is the native mouse event that began the interaction.
10532 unselect: function(ev
) {
10533 if (this.isSelected
) {
10534 this.isSelected
= false;
10535 if (this.destroySelection
) {
10536 this.destroySelection(); // TODO: deprecate
10538 this.unrenderSelection();
10539 this.publiclyTrigger('unselect', {
10548 ------------------------------------------------------------------------------------------------------------------*/
10551 selectEventInstance: function(eventInstance
) {
10553 !this.selectedEventInstance
||
10554 this.selectedEventInstance
!== eventInstance
10556 this.unselectEventInstance();
10558 this.getEventSegs().forEach(function(seg
) {
10560 seg
.footprint
.eventInstance
=== eventInstance
&&
10561 seg
.el
// necessary?
10563 seg
.el
.addClass('fc-selected');
10567 this.selectedEventInstance
= eventInstance
;
10572 unselectEventInstance: function() {
10573 if (this.selectedEventInstance
) {
10575 this.getEventSegs().forEach(function(seg
) {
10576 if (seg
.el
) { // necessary?
10577 seg
.el
.removeClass('fc-selected');
10581 this.selectedEventInstance
= null;
10586 isEventDefSelected: function(eventDef
) {
10587 // event references might change on refetchEvents(), while selectedEventInstance doesn't,
10589 return this.selectedEventInstance
&& this.selectedEventInstance
.def
.id
=== eventDef
.id
;
10593 /* Mouse / Touch Unselecting (time range & event unselection)
10594 ------------------------------------------------------------------------------------------------------------------*/
10595 // TODO: move consistently to down/start or up/end?
10596 // TODO: don't kill previous selection if touch scrolling
10599 handleDocumentMousedown: function(ev
) {
10600 if (isPrimaryMouseButton(ev
)) {
10601 this.processUnselect(ev
);
10606 processUnselect: function(ev
) {
10607 this.processRangeUnselect(ev
);
10608 this.processEventUnselect(ev
);
10612 processRangeUnselect: function(ev
) {
10615 // is there a time-range selection?
10616 if (this.isSelected
&& this.opt('unselectAuto')) {
10617 // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
10618 ignore
= this.opt('unselectCancel');
10619 if (!ignore
|| !$(ev
.target
).closest(ignore
).length
) {
10626 processEventUnselect: function(ev
) {
10627 if (this.selectedEventInstance
) {
10628 if (!$(ev
.target
).closest('.fc-selected').length
) {
10629 this.unselectEventInstance();
10636 ------------------------------------------------------------------------------------------------------------------*/
10639 // Triggers handlers to 'dayClick'
10640 // Span has start/end of the clicked area. Only the start is useful.
10641 triggerDayClick: function(footprint
, dayEl
, ev
) {
10642 var dateProfile
= this.calendar
.footprintToDateProfile(footprint
); // abuse of "Event"DateProfile?
10644 this.publiclyTrigger('dayClick', {
10646 args
: [ dateProfile
.start
, ev
, this ]
10653 View
.watch('displayingDates', [ 'dateProfile' ], function(deps
) {
10654 this.requestDateRender(deps
.dateProfile
);
10656 this.requestDateUnrender();
10660 View
.watch('initialEvents', [ 'dateProfile' ], function(deps
) {
10661 return this.fetchInitialEvents(deps
.dateProfile
);
10665 View
.watch('bindingEvents', [ 'initialEvents' ], function(deps
) {
10666 this.setEvents(deps
.initialEvents
);
10667 this.bindEventChanges();
10669 this.unbindEventChanges();
10670 this.unsetEvents();
10674 View
.watch('displayingEvents', [ 'displayingDates', 'hasEvents' ], function() {
10675 this.requestEventsRender(this.get('currentEvents')); // if there were event mutations after initialEvents
10677 this.requestEventsUnrender();
10681 function convertEventsPayloadToLegacyArray(eventsPayload
) {
10682 var legacyEvents
= [];
10684 var eventInstances
;
10687 for (id
in eventsPayload
) {
10689 eventInstances
= eventsPayload
[id
].eventInstances
;
10691 for (i
= 0; i
< eventInstances
.length
; i
++) {
10693 eventInstances
[i
].toLegacy()
10698 return legacyEvents
;
10705 // range the view is formally responsible for.
10706 // for example, a month view might have 1st-31st, excluding padded dates
10707 currentUnzonedRange
: null,
10708 currentRangeUnit
: null, // name of largest unit being displayed, like "month" or "week"
10710 isRangeAllDay
: false,
10712 // date range with a rendered skeleton
10713 // includes not-active days that need some sort of DOM
10714 renderUnzonedRange
: null,
10716 // dates that display events and accept drag-n-drop
10717 activeUnzonedRange
: null,
10719 // constraint for where prev/next operations can go and where events can be dragged/resized to.
10720 // an object with optional start and end properties.
10721 validUnzonedRange
: null,
10723 // how far the current date will move for a prev/next operation
10724 dateIncrement
: null,
10726 minTime
: null, // Duration object that denotes the first visible time of any given day
10727 maxTime
: null, // Duration object that denotes the exclusive visible end time of any given day
10728 usesMinMaxTime
: false, // whether minTime/maxTime will affect the activeUnzonedRange. Views must opt-in.
10731 start
: null, // use activeUnzonedRange
10732 end
: null, // use activeUnzonedRange
10733 intervalStart
: null, // use currentUnzonedRange
10734 intervalEnd
: null, // use currentUnzonedRange
10737 /* Date Range Computation
10738 ------------------------------------------------------------------------------------------------------------------*/
10741 setDateProfileForRendering: function(dateProfile
) {
10742 var calendar
= this.calendar
;
10744 this.currentUnzonedRange
= dateProfile
.currentUnzonedRange
;
10745 this.currentRangeUnit
= dateProfile
.currentRangeUnit
;
10746 this.isRangeAllDay
= dateProfile
.isRangeAllDay
;
10747 this.renderUnzonedRange
= dateProfile
.renderUnzonedRange
;
10748 this.activeUnzonedRange
= dateProfile
.activeUnzonedRange
;
10749 this.validUnzonedRange
= dateProfile
.validUnzonedRange
;
10750 this.dateIncrement
= dateProfile
.dateIncrement
;
10751 this.minTime
= dateProfile
.minTime
;
10752 this.maxTime
= dateProfile
.maxTime
;
10754 // DEPRECATED, but we need to keep it updated...
10755 this.start
= calendar
.msToMoment(dateProfile
.activeUnzonedRange
.startMs
, this.isRangeAllDay
);
10756 this.end
= calendar
.msToMoment(dateProfile
.activeUnzonedRange
.endMs
, this.isRangeAllDay
);
10757 this.intervalStart
= calendar
.msToMoment(dateProfile
.currentUnzonedRange
.startMs
, this.isRangeAllDay
);
10758 this.intervalEnd
= calendar
.msToMoment(dateProfile
.currentUnzonedRange
.endMs
, this.isRangeAllDay
);
10760 this.title
= this.computeTitle();
10761 this.calendar
.reportViewDatesChanged(this, dateProfile
);
10765 // Builds a structure with info about what the dates/ranges will be for the "prev" view.
10766 buildPrevDateProfile: function(date
) {
10767 var prevDate
= date
.clone().startOf(this.currentRangeUnit
).subtract(this.dateIncrement
);
10769 return this.buildDateProfile(prevDate
, -1);
10773 // Builds a structure with info about what the dates/ranges will be for the "next" view.
10774 buildNextDateProfile: function(date
) {
10775 var nextDate
= date
.clone().startOf(this.currentRangeUnit
).add(this.dateIncrement
);
10777 return this.buildDateProfile(nextDate
, 1);
10781 // Builds a structure holding dates/ranges for rendering around the given date.
10782 // Optional direction param indicates whether the date is being incremented/decremented
10783 // from its previous value. decremented = -1, incremented = 1 (default).
10784 buildDateProfile: function(date
, direction
, forceToValid
) {
10785 var isDateAllDay
= !date
.hasTime();
10786 var validUnzonedRange
= this.buildValidRange();
10787 var minTime
= null;
10788 var maxTime
= null;
10790 var renderUnzonedRange
;
10791 var activeUnzonedRange
;
10794 if (forceToValid
) {
10795 date
= this.calendar
.msToUtcMoment(
10796 validUnzonedRange
.constrainDate(date
), // returns MS
10801 currentInfo
= this.buildCurrentRangeInfo(date
, direction
);
10802 renderUnzonedRange
= this.buildRenderRange(currentInfo
.unzonedRange
, currentInfo
.unit
);
10803 activeUnzonedRange
= renderUnzonedRange
.clone();
10805 if (!this.opt('showNonCurrentDates')) {
10806 activeUnzonedRange
= activeUnzonedRange
.intersect(currentInfo
.unzonedRange
);
10809 minTime
= moment
.duration(this.opt('minTime'));
10810 maxTime
= moment
.duration(this.opt('maxTime'));
10811 activeUnzonedRange
= this.adjustActiveRange(activeUnzonedRange
, minTime
, maxTime
);
10813 activeUnzonedRange
= activeUnzonedRange
.intersect(validUnzonedRange
);
10815 if (activeUnzonedRange
) {
10816 date
= this.calendar
.msToUtcMoment(
10817 activeUnzonedRange
.constrainDate(date
), // returns MS
10822 // it's invalid if the originally requested date is not contained,
10823 // or if the range is completely outside of the valid range.
10824 isValid
= currentInfo
.unzonedRange
.intersectsWith(validUnzonedRange
);
10827 validUnzonedRange
: validUnzonedRange
,
10828 currentUnzonedRange
: currentInfo
.unzonedRange
,
10829 currentRangeUnit
: currentInfo
.unit
,
10830 isRangeAllDay
: /^(year|month|week|day)$/.test(currentInfo
.unit
),
10831 activeUnzonedRange
: activeUnzonedRange
,
10832 renderUnzonedRange
: renderUnzonedRange
,
10837 dateIncrement
: this.buildDateIncrement(currentInfo
.duration
)
10838 // pass a fallback (might be null) ^
10843 // Builds an object with optional start/end properties.
10844 // Indicates the minimum/maximum dates to display.
10845 buildValidRange: function() {
10846 return this.getUnzonedRangeOption('validRange', this.calendar
.getNow()) ||
10847 new UnzonedRange(); // completely open-ended
10851 // Builds a structure with info about the "current" range, the range that is
10852 // highlighted as being the current month for example.
10853 // See buildDateProfile for a description of `direction`.
10854 // Guaranteed to have `range` and `unit` properties. `duration` is optional.
10855 // TODO: accept a MS-time instead of a moment `date`?
10856 buildCurrentRangeInfo: function(date
, direction
) {
10857 var duration
= null;
10859 var unzonedRange
= null;
10862 if (this.viewSpec
.duration
) {
10863 duration
= this.viewSpec
.duration
;
10864 unit
= this.viewSpec
.durationUnit
;
10865 unzonedRange
= this.buildRangeFromDuration(date
, direction
, duration
, unit
);
10867 else if ((dayCount
= this.opt('dayCount'))) {
10869 unzonedRange
= this.buildRangeFromDayCount(date
, direction
, dayCount
);
10871 else if ((unzonedRange
= this.buildCustomVisibleRange(date
))) {
10872 unit
= computeGreatestUnit(unzonedRange
.getStart(), unzonedRange
.getEnd());
10875 duration
= this.getFallbackDuration();
10876 unit
= computeGreatestUnit(duration
);
10877 unzonedRange
= this.buildRangeFromDuration(date
, direction
, duration
, unit
);
10880 return { duration
: duration
, unit
: unit
, unzonedRange
: unzonedRange
};
10884 getFallbackDuration: function() {
10885 return moment
.duration({ days
: 1 });
10889 // Returns a new activeUnzonedRange to have time values (un-ambiguate)
10890 // minTime or maxTime causes the range to expand.
10891 adjustActiveRange: function(unzonedRange
, minTime
, maxTime
) {
10892 var start
= unzonedRange
.getStart();
10893 var end
= unzonedRange
.getEnd();
10895 if (this.usesMinMaxTime
) {
10898 start
.time(0).add(minTime
);
10901 if (maxTime
> 24 * 60 * 60 * 1000) { // beyond 24 hours?
10902 end
.time(maxTime
- (24 * 60 * 60 * 1000));
10906 return new UnzonedRange(start
, end
);
10910 // Builds the "current" range when it is specified as an explicit duration.
10911 // `unit` is the already-computed computeGreatestUnit value of duration.
10912 // TODO: accept a MS-time instead of a moment `date`?
10913 buildRangeFromDuration: function(date
, direction
, duration
, unit
) {
10914 var alignment
= this.opt('dateAlignment');
10915 var start
= date
.clone();
10917 var dateIncrementInput
;
10918 var dateIncrementDuration
;
10920 // if the view displays a single day or smaller
10921 if (duration
.as('days') <= 1) {
10922 if (this.isHiddenDay(start
)) {
10923 start
= this.skipHiddenDays(start
, direction
);
10924 start
.startOf('day');
10928 // compute what the alignment should be
10930 dateIncrementInput
= this.opt('dateIncrement');
10932 if (dateIncrementInput
) {
10933 dateIncrementDuration
= moment
.duration(dateIncrementInput
);
10935 // use the smaller of the two units
10936 if (dateIncrementDuration
< duration
) {
10937 alignment
= computeDurationGreatestUnit(dateIncrementDuration
, dateIncrementInput
);
10948 start
.startOf(alignment
);
10949 end
= start
.clone().add(duration
);
10951 return new UnzonedRange(start
, end
);
10955 // Builds the "current" range when a dayCount is specified.
10956 // TODO: accept a MS-time instead of a moment `date`?
10957 buildRangeFromDayCount: function(date
, direction
, dayCount
) {
10958 var customAlignment
= this.opt('dateAlignment');
10959 var runningCount
= 0;
10960 var start
= date
.clone();
10963 if (customAlignment
) {
10964 start
.startOf(customAlignment
);
10967 start
.startOf('day');
10968 start
= this.skipHiddenDays(start
, direction
);
10970 end
= start
.clone();
10973 if (!this.isHiddenDay(end
)) {
10976 } while (runningCount
< dayCount
);
10978 return new UnzonedRange(start
, end
);
10982 // Builds a normalized range object for the "visible" range,
10983 // which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
10984 // TODO: accept a MS-time instead of a moment `date`?
10985 buildCustomVisibleRange: function(date
) {
10986 var visibleUnzonedRange
= this.getUnzonedRangeOption(
10988 this.calendar
.applyTimezone(date
) // correct zone. also generates new obj that avoids mutations
10991 if (visibleUnzonedRange
&& (visibleUnzonedRange
.startMs
=== null || visibleUnzonedRange
.endMs
=== null)) {
10995 return visibleUnzonedRange
;
10999 // Computes the range that will represent the element/cells for *rendering*,
11000 // but which may have voided days/times.
11001 buildRenderRange: function(currentUnzonedRange
, currentRangeUnit
) {
11002 // cut off days in the currentUnzonedRange that are hidden
11003 return this.trimHiddenDays(currentUnzonedRange
);
11007 // Compute the duration value that should be added/substracted to the current date
11008 // when a prev/next operation happens.
11009 buildDateIncrement: function(fallback
) {
11010 var dateIncrementInput
= this.opt('dateIncrement');
11011 var customAlignment
;
11013 if (dateIncrementInput
) {
11014 return moment
.duration(dateIncrementInput
);
11016 else if ((customAlignment
= this.opt('dateAlignment'))) {
11017 return moment
.duration(1, customAlignment
);
11019 else if (fallback
) {
11023 return moment
.duration({ days
: 1 });
11028 // Remove days from the beginning and end of the range that are computed as hidden.
11029 trimHiddenDays: function(inputUnzonedRange
) {
11030 var start
= inputUnzonedRange
.getStart();
11031 var end
= inputUnzonedRange
.getEnd();
11033 start
= this.skipHiddenDays(start
);
11034 end
= this.skipHiddenDays(end
, -1, true);
11036 return new UnzonedRange(start
, end
);
11040 // Compute the number of the give units in the "current" range.
11041 // Will return a floating-point number. Won't round.
11042 currentRangeAs: function(unit
) {
11043 var currentUnzonedRange
= this.currentUnzonedRange
;
11045 return moment
.utc(currentUnzonedRange
.endMs
).diff(
11046 moment
.utc(currentUnzonedRange
.startMs
),
11053 // For ChronoComponent::getDayClasses
11054 isDateInOtherMonth: function(date
) {
11059 // Arguments after name will be forwarded to a hypothetical function value
11060 // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
11061 // Always clone your objects if you fear mutation.
11062 getUnzonedRangeOption: function(name
) {
11063 var val
= this.opt(name
);
11065 if (typeof val
=== 'function') {
11068 Array
.prototype.slice
.call(arguments
, 1)
11073 return this.calendar
.parseUnzonedRange(val
);
11079 ------------------------------------------------------------------------------------------------------------------*/
11082 // Initializes internal variables related to calculating hidden days-of-week
11083 initHiddenDays: function() {
11084 var hiddenDays
= this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
11085 var isHiddenDayHash
= []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
11089 if (this.opt('weekends') === false) {
11090 hiddenDays
.push(0, 6); // 0=sunday, 6=saturday
11093 for (i
= 0; i
< 7; i
++) {
11095 !(isHiddenDayHash
[i
] = $.inArray(i
, hiddenDays
) !== -1)
11102 throw 'invalid hiddenDays'; // all days were hidden? bad.
11105 this.isHiddenDayHash
= isHiddenDayHash
;
11109 // Is the current day hidden?
11110 // `day` is a day-of-week index (0-6), or a Moment
11111 isHiddenDay: function(day
) {
11112 if (moment
.isMoment(day
)) {
11115 return this.isHiddenDayHash
[day
];
11119 // Incrementing the current day until it is no longer a hidden day, returning a copy.
11120 // DOES NOT CONSIDER validUnzonedRange!
11121 // If the initial value of `date` is not a hidden day, don't do anything.
11122 // Pass `isExclusive` as `true` if you are dealing with an end date.
11123 // `inc` defaults to `1` (increment one day forward each time)
11124 skipHiddenDays: function(date
, inc
, isExclusive
) {
11125 var out
= date
.clone();
11128 this.isHiddenDayHash
[(out
.day() + (isExclusive
? inc
: 0) + 7) % 7]
11130 out
.add(inc
, 'days');
11140 Embodies a div that has potential scrollbars
11142 var Scroller
= FC
.Scroller
= Class
.extend({
11144 el
: null, // the guaranteed outer element
11145 scrollEl
: null, // the element with the scrollbars
11150 constructor: function(options
) {
11151 options
= options
|| {};
11152 this.overflowX
= options
.overflowX
|| options
.overflow
|| 'auto';
11153 this.overflowY
= options
.overflowY
|| options
.overflow
|| 'auto';
11157 render: function() {
11158 this.el
= this.renderEl();
11159 this.applyOverflow();
11163 renderEl: function() {
11164 return (this.scrollEl
= $('<div class="fc-scroller"></div>'));
11168 // sets to natural height, unlocks overflow
11169 clear: function() {
11170 this.setHeight('auto');
11171 this.applyOverflow();
11175 destroy: function() {
11181 // -----------------------------------------------------------------------------------------------------------------
11184 applyOverflow: function() {
11185 this.scrollEl
.css({
11186 'overflow-x': this.overflowX
,
11187 'overflow-y': this.overflowY
11192 // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
11193 // Useful for preserving scrollbar widths regardless of future resizes.
11194 // Can pass in scrollbarWidths for optimization.
11195 lockOverflow: function(scrollbarWidths
) {
11196 var overflowX
= this.overflowX
;
11197 var overflowY
= this.overflowY
;
11199 scrollbarWidths
= scrollbarWidths
|| this.getScrollbarWidths();
11201 if (overflowX
=== 'auto') {
11203 scrollbarWidths
.top
|| scrollbarWidths
.bottom
|| // horizontal scrollbars?
11204 // OR scrolling pane with massless scrollbars?
11205 this.scrollEl
[0].scrollWidth
- 1 > this.scrollEl
[0].clientWidth
11206 // subtract 1 because of IE off-by-one issue
11207 ) ? 'scroll' : 'hidden';
11210 if (overflowY
=== 'auto') {
11212 scrollbarWidths
.left
|| scrollbarWidths
.right
|| // vertical scrollbars?
11213 // OR scrolling pane with massless scrollbars?
11214 this.scrollEl
[0].scrollHeight
- 1 > this.scrollEl
[0].clientHeight
11215 // subtract 1 because of IE off-by-one issue
11216 ) ? 'scroll' : 'hidden';
11219 this.scrollEl
.css({ 'overflow-x': overflowX
, 'overflow-y': overflowY
});
11223 // Getters / Setters
11224 // -----------------------------------------------------------------------------------------------------------------
11227 setHeight: function(height
) {
11228 this.scrollEl
.height(height
);
11232 getScrollTop: function() {
11233 return this.scrollEl
.scrollTop();
11237 setScrollTop: function(top
) {
11238 this.scrollEl
.scrollTop(top
);
11242 getClientWidth: function() {
11243 return this.scrollEl
[0].clientWidth
;
11247 getClientHeight: function() {
11248 return this.scrollEl
[0].clientHeight
;
11252 getScrollbarWidths: function() {
11253 return getScrollbarWidths(this.scrollEl
);
11259 function Iterator(items
) {
11260 this.items
= items
|| [];
11264 /* Calls a method on every item passing the arguments through */
11265 Iterator
.prototype.proxyCall = function(methodName
) {
11266 var args
= Array
.prototype.slice
.call(arguments
, 1);
11269 this.items
.forEach(function(item
) {
11270 results
.push(item
[methodName
].apply(item
, args
));
11278 /* Toolbar with buttons and title
11279 ----------------------------------------------------------------------------------------------------------------------*/
11281 function Toolbar(calendar
, toolbarOptions
) {
11285 t
.setToolbarOptions
= setToolbarOptions
;
11287 t
.removeElement
= removeElement
;
11288 t
.updateTitle
= updateTitle
;
11289 t
.activateButton
= activateButton
;
11290 t
.deactivateButton
= deactivateButton
;
11291 t
.disableButton
= disableButton
;
11292 t
.enableButton
= enableButton
;
11293 t
.getViewsWithButtons
= getViewsWithButtons
;
11294 t
.el
= null; // mirrors local `el`
11298 var viewsWithButtons
= [];
11300 // method to update toolbar-specific options, not calendar-wide options
11301 function setToolbarOptions(newToolbarOptions
) {
11302 toolbarOptions
= newToolbarOptions
;
11305 // can be called repeatedly and will rerender
11306 function render() {
11307 var sections
= toolbarOptions
.layout
;
11311 el
= this.el
= $("<div class='fc-toolbar "+ toolbarOptions
.extraClasses
+ "'/>");
11316 el
.append(renderSection('left'))
11317 .append(renderSection('right'))
11318 .append(renderSection('center'))
11319 .append('<div class="fc-clear"/>');
11327 function removeElement() {
11335 function renderSection(position
) {
11336 var theme
= calendar
.theme
;
11337 var sectionEl
= $('<div class="fc-' + position
+ '"/>');
11338 var buttonStr
= toolbarOptions
.layout
[position
];
11339 var calendarCustomButtons
= calendar
.opt('customButtons') || {};
11340 var calendarButtonTextOverrides
= calendar
.overrides
.buttonText
|| {};
11341 var calendarButtonText
= calendar
.opt('buttonText') || {};
11344 $.each(buttonStr
.split(' '), function(i
) {
11345 var groupChildren
= $();
11346 var isOnlyButtons
= true;
11349 $.each(this.split(','), function(j
, buttonName
) {
11350 var customButtonProps
;
11353 var buttonIcon
; // only one of these will be set
11354 var buttonText
; // "
11355 var buttonInnerHtml
;
11359 if (buttonName
== 'title') {
11360 groupChildren
= groupChildren
.add($('<h2> </h2>')); // we always want it to take up height
11361 isOnlyButtons
= false;
11365 if ((customButtonProps
= calendarCustomButtons
[buttonName
])) {
11366 buttonClick = function(ev
) {
11367 if (customButtonProps
.click
) {
11368 customButtonProps
.click
.call(buttonEl
[0], ev
);
11371 (buttonIcon
= theme
.getCustomButtonIconClass(customButtonProps
)) ||
11372 (buttonIcon
= theme
.getIconClass(buttonName
)) ||
11373 (buttonText
= customButtonProps
.text
); // jshint ignore:line
11375 else if ((viewSpec
= calendar
.getViewSpec(buttonName
))) {
11376 viewsWithButtons
.push(buttonName
);
11377 buttonClick = function() {
11378 calendar
.changeView(buttonName
);
11380 (buttonText
= viewSpec
.buttonTextOverride
) ||
11381 (buttonIcon
= theme
.getIconClass(buttonName
)) ||
11382 (buttonText
= viewSpec
.buttonTextDefault
); // jshint ignore:line
11384 else if (calendar
[buttonName
]) { // a calendar method
11385 buttonClick = function() {
11386 calendar
[buttonName
]();
11388 (buttonText
= calendarButtonTextOverrides
[buttonName
]) ||
11389 (buttonIcon
= theme
.getIconClass(buttonName
)) ||
11390 (buttonText
= calendarButtonText
[buttonName
]); // jshint ignore:line
11391 // ^ everything else is considered default
11397 'fc-' + buttonName
+ '-button',
11398 theme
.getClass('button'),
11399 theme
.getClass('stateDefault')
11403 buttonInnerHtml
= htmlEscape(buttonText
);
11405 else if (buttonIcon
) {
11406 buttonInnerHtml
= "<span class='" + buttonIcon
+ "'></span>";
11409 buttonEl
= $( // type="button" so that it doesn't submit a form
11410 '<button type="button" class="' + buttonClasses
.join(' ') + '">' +
11414 .click(function(ev
) {
11415 // don't process clicks for disabled buttons
11416 if (!buttonEl
.hasClass(theme
.getClass('stateDisabled'))) {
11420 // after the click action, if the button becomes the "active" tab, or disabled,
11421 // it should never have a hover class, so remove it now.
11423 buttonEl
.hasClass(theme
.getClass('stateActive')) ||
11424 buttonEl
.hasClass(theme
.getClass('stateDisabled'))
11426 buttonEl
.removeClass(theme
.getClass('stateHover'));
11430 .mousedown(function() {
11431 // the *down* effect (mouse pressed in).
11432 // only on buttons that are not the "active" tab, or disabled
11434 .not('.' + theme
.getClass('stateActive'))
11435 .not('.' + theme
.getClass('stateDisabled'))
11436 .addClass(theme
.getClass('stateDown'));
11438 .mouseup(function() {
11439 // undo the *down* effect
11440 buttonEl
.removeClass(theme
.getClass('stateDown'));
11444 // the *hover* effect.
11445 // only on buttons that are not the "active" tab, or disabled
11447 .not('.' + theme
.getClass('stateActive'))
11448 .not('.' + theme
.getClass('stateDisabled'))
11449 .addClass(theme
.getClass('stateHover'));
11452 // undo the *hover* effect
11454 .removeClass(theme
.getClass('stateHover'))
11455 .removeClass(theme
.getClass('stateDown')); // if mouseleave happens before mouseup
11459 groupChildren
= groupChildren
.add(buttonEl
);
11464 if (isOnlyButtons
) {
11466 .first().addClass(theme
.getClass('cornerLeft')).end()
11467 .last().addClass(theme
.getClass('cornerRight')).end();
11470 if (groupChildren
.length
> 1) {
11471 groupEl
= $('<div/>');
11472 if (isOnlyButtons
) {
11473 groupEl
.addClass(theme
.getClass('buttonGroup'));
11475 groupEl
.append(groupChildren
);
11476 sectionEl
.append(groupEl
);
11479 sectionEl
.append(groupChildren
); // 1 or 0 children
11488 function updateTitle(text
) {
11490 el
.find('h2').text(text
);
11495 function activateButton(buttonName
) {
11497 el
.find('.fc-' + buttonName
+ '-button')
11498 .addClass(calendar
.theme
.getClass('stateActive'));
11503 function deactivateButton(buttonName
) {
11505 el
.find('.fc-' + buttonName
+ '-button')
11506 .removeClass(calendar
.theme
.getClass('stateActive'));
11511 function disableButton(buttonName
) {
11513 el
.find('.fc-' + buttonName
+ '-button')
11514 .prop('disabled', true)
11515 .addClass(calendar
.theme
.getClass('stateDisabled'));
11520 function enableButton(buttonName
) {
11522 el
.find('.fc-' + buttonName
+ '-button')
11523 .prop('disabled', false)
11524 .removeClass(calendar
.theme
.getClass('stateDisabled'));
11529 function getViewsWithButtons() {
11530 return viewsWithButtons
;
11537 var Calendar
= FC
.Calendar
= Class
.extend(EmitterMixin
, {
11539 view
: null, // current View object
11540 viewsByType
: null, // holds all instantiated view instances, current or not
11541 currentDate
: null, // unzoned moment. private (public API should use getDate instead)
11543 loadingLevel
: 0, // number of simultaneous loading tasks
11546 constructor: function(el
, overrides
) {
11548 // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
11549 // unneeded() is called in destroy.
11550 GlobalEmitter
.needed();
11553 this.viewsByType
= {};
11554 this.viewSpecCache
= {};
11556 this.initOptionsInternals(overrides
);
11557 this.initMomentInternals(); // needs to happen after options hash initialized
11558 this.initCurrentDate();
11559 this.initEventManager();
11561 EventManager
.call(this); // needs options immediately
11566 // Subclasses can override this for initialization logic after the constructor has been called
11567 initialize: function() {
11572 // -----------------------------------------------------------------------------------------------------------------
11575 getView: function() {
11580 publiclyTrigger: function(name
, triggerInfo
) {
11581 var optHandler
= this.opt(name
);
11585 if ($.isPlainObject(triggerInfo
)) {
11586 context
= triggerInfo
.context
;
11587 args
= triggerInfo
.args
;
11589 else if ($.isArray(triggerInfo
)) {
11590 args
= triggerInfo
;
11593 if (context
== null) {
11594 context
= this.el
[0]; // fallback context
11601 this.triggerWith(name
, context
, args
); // Emitter's method
11604 return optHandler
.apply(context
, args
);
11609 hasPublicHandlers: function(name
) {
11610 return this.hasHandlers(name
) ||
11611 this.opt(name
); // handler specified in options
11616 // -----------------------------------------------------------------------------------------------------------------
11619 // Given a view name for a custom view or a standard view, creates a ready-to-go View object
11620 instantiateView: function(viewType
) {
11621 var spec
= this.getViewSpec(viewType
);
11623 return new spec
['class'](this, spec
);
11627 // Returns a boolean about whether the view is okay to instantiate at some point
11628 isValidViewType: function(viewType
) {
11629 return Boolean(this.getViewSpec(viewType
));
11633 changeView: function(viewName
, dateOrRange
) {
11637 if (dateOrRange
.start
&& dateOrRange
.end
) { // a range
11638 this.recordOptionOverrides({ // will not rerender
11639 visibleRange
: dateOrRange
11643 this.currentDate
= this.moment(dateOrRange
).stripZone(); // just like gotoDate
11647 this.renderView(viewName
);
11651 // Forces navigation to a view for the given date.
11652 // `viewType` can be a specific view name or a generic one like "week" or "day".
11653 zoomTo: function(newDate
, viewType
) {
11656 viewType
= viewType
|| 'day'; // day is default zoom
11657 spec
= this.getViewSpec(viewType
) || this.getUnitViewSpec(viewType
);
11659 this.currentDate
= newDate
.clone();
11660 this.renderView(spec
? spec
.type
: null);
11665 // -----------------------------------------------------------------------------------------------------------------
11668 initCurrentDate: function() {
11669 var defaultDateInput
= this.opt('defaultDate');
11671 // compute the initial ambig-timezone date
11672 if (defaultDateInput
!= null) {
11673 this.currentDate
= this.moment(defaultDateInput
).stripZone();
11676 this.currentDate
= this.getNow(); // getNow already returns unzoned
11681 reportViewDatesChanged: function(view
, dateProfile
) {
11682 this.currentDate
= dateProfile
.date
; // might have been constrained by view dates
11683 this.setToolbarsTitle(view
.title
);
11684 this.updateToolbarButtons();
11689 var prevInfo
= this.view
.buildPrevDateProfile(this.currentDate
);
11691 if (prevInfo
.isValid
) {
11692 this.currentDate
= prevInfo
.date
;
11699 var nextInfo
= this.view
.buildNextDateProfile(this.currentDate
);
11701 if (nextInfo
.isValid
) {
11702 this.currentDate
= nextInfo
.date
;
11708 prevYear: function() {
11709 this.currentDate
.add(-1, 'years');
11714 nextYear: function() {
11715 this.currentDate
.add(1, 'years');
11720 today: function() {
11721 this.currentDate
= this.getNow(); // should deny like prev/next?
11726 gotoDate: function(zonedDateInput
) {
11727 this.currentDate
= this.moment(zonedDateInput
).stripZone();
11732 incrementDate: function(delta
) {
11733 this.currentDate
.add(moment
.duration(delta
));
11738 // for external API
11739 getDate: function() {
11740 return this.applyTimezone(this.currentDate
); // infuse the calendar's timezone
11744 // Loading Triggering
11745 // -----------------------------------------------------------------------------------------------------------------
11748 // Should be called when any type of async data fetching begins
11749 pushLoading: function() {
11750 if (!(this.loadingLevel
++)) {
11751 this.publiclyTrigger('loading', [ true, this.view
]);
11756 // Should be called when any type of async data fetching completes
11757 popLoading: function() {
11758 if (!(--this.loadingLevel
)) {
11759 this.publiclyTrigger('loading', [ false, this.view
]);
11765 // -----------------------------------------------------------------------------------------------------------------
11768 // this public method receives start/end dates in any format, with any timezone
11769 select: function(zonedStartInput
, zonedEndInput
) {
11771 this.buildSelectFootprint
.apply(this, arguments
)
11776 unselect: function() { // safe to be called before renderView
11778 this.view
.unselect();
11783 // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
11784 buildSelectFootprint: function(zonedStartInput
, zonedEndInput
) {
11785 var start
= this.moment(zonedStartInput
).stripZone();
11788 if (zonedEndInput
) {
11789 end
= this.moment(zonedEndInput
).stripZone();
11791 else if (start
.hasTime()) {
11792 end
= start
.clone().add(this.defaultTimedEventDuration
);
11795 end
= start
.clone().add(this.defaultAllDayEventDuration
);
11798 return new ComponentFootprint(
11799 new UnzonedRange(start
, end
),
11806 // -----------------------------------------------------------------------------------------------------------------
11809 // will return `null` if invalid range
11810 parseUnzonedRange: function(rangeInput
) {
11814 if (rangeInput
.start
) {
11815 start
= this.moment(rangeInput
.start
).stripZone();
11818 if (rangeInput
.end
) {
11819 end
= this.moment(rangeInput
.end
).stripZone();
11822 if (!start
&& !end
) {
11826 if (start
&& end
&& end
.isBefore(start
)) {
11830 return new UnzonedRange(start
, end
);
11834 rerenderEvents: function() { // API method. destroys old events if previously rendered.
11835 if (this.elementVisible()) {
11836 this.view
.flash('displayingEvents');
11841 initEventManager: function() {
11843 var eventManager
= new EventManager(this);
11844 var rawSources
= this.opt('eventSources') || [];
11845 var singleRawSource
= this.opt('events');
11847 this.eventManager
= eventManager
;
11849 if (singleRawSource
) {
11850 rawSources
.unshift(singleRawSource
);
11853 eventManager
.on('release', function(eventsPayload
) {
11854 _this
.trigger('eventsReset', eventsPayload
);
11857 eventManager
.freeze();
11859 rawSources
.forEach(function(rawSource
) {
11860 var source
= EventSourceParser
.parse(rawSource
, _this
);
11863 eventManager
.addSource(source
);
11867 eventManager
.thaw();
11871 requestEvents: function(start
, end
) {
11872 return this.eventManager
.requestEvents(
11875 this.opt('timezone'),
11876 !this.opt('lazyFetching')
11884 Options binding/triggering system.
11888 dirDefaults
: null, // option defaults related to LTR or RTL
11889 localeDefaults
: null, // option defaults related to current locale
11890 overrides
: null, // option overrides given to the fullCalendar constructor
11891 dynamicOverrides
: null, // options set with dynamic setter method. higher precedence than view overrides.
11892 optionsModel
: null, // all defaults combined with overrides
11895 initOptionsInternals: function(overrides
) {
11896 this.overrides
= $.extend({}, overrides
); // make a copy
11897 this.dynamicOverrides
= {};
11898 this.optionsModel
= new Model();
11900 this.populateOptionsHash();
11904 // public getter/setter
11905 option: function(name
, value
) {
11908 if (typeof name
=== 'string') {
11909 if (value
=== undefined) { // getter
11910 return this.optionsModel
.get(name
);
11912 else { // setter for individual option
11913 newOptionHash
= {};
11914 newOptionHash
[name
] = value
;
11915 this.setOptions(newOptionHash
);
11918 else if (typeof name
=== 'object') { // compound setter with object input
11919 this.setOptions(name
);
11925 opt: function(name
) {
11926 return this.optionsModel
.get(name
);
11930 setOptions: function(newOptionHash
) {
11934 this.recordOptionOverrides(newOptionHash
); // will trigger optionsModel watchers
11936 for (optionName
in newOptionHash
) {
11940 // special-case handling of single option change.
11941 // if only one option change, `optionName` will be its name.
11942 if (optionCnt
=== 1) {
11943 if (optionName
=== 'height' || optionName
=== 'contentHeight' || optionName
=== 'aspectRatio') {
11944 this.updateSize(true); // true = allow recalculation of height
11947 else if (optionName
=== 'defaultDate') {
11948 return; // can't change date this way. use gotoDate instead
11950 else if (optionName
=== 'businessHours') {
11952 this.view
.unrenderBusinessHours();
11953 this.view
.renderBusinessHours();
11957 else if (optionName
=== 'timezone') {
11958 this.view
.flash('initialEvents');
11963 // catch-all. rerender the header and footer and rebuild/rerender the current view
11964 this.renderHeader();
11965 this.renderFooter();
11967 // even non-current views will be affected by this option change. do before rerender
11969 this.viewsByType
= {};
11975 // Computes the flattened options hash for the calendar and assigns to `this.options`.
11976 // Assumes this.overrides and this.dynamicOverrides have already been initialized.
11977 populateOptionsHash: function() {
11978 var locale
, localeDefaults
;
11979 var isRTL
, dirDefaults
;
11982 locale
= firstDefined( // explicit locale option given?
11983 this.dynamicOverrides
.locale
,
11984 this.overrides
.locale
11986 localeDefaults
= localeOptionHash
[locale
];
11987 if (!localeDefaults
) { // explicit locale option not given or invalid?
11988 locale
= Calendar
.defaults
.locale
;
11989 localeDefaults
= localeOptionHash
[locale
] || {};
11992 isRTL
= firstDefined( // based on options computed so far, is direction RTL?
11993 this.dynamicOverrides
.isRTL
,
11994 this.overrides
.isRTL
,
11995 localeDefaults
.isRTL
,
11996 Calendar
.defaults
.isRTL
11998 dirDefaults
= isRTL
? Calendar
.rtlDefaults
: {};
12000 this.dirDefaults
= dirDefaults
;
12001 this.localeDefaults
= localeDefaults
;
12003 rawOptions
= mergeOptions([ // merge defaults and overrides. lowest to highest precedence
12004 Calendar
.defaults
, // global defaults
12008 this.dynamicOverrides
12010 populateInstanceComputableOptions(rawOptions
); // fill in gaps with computed options
12012 this.optionsModel
.reset(rawOptions
);
12016 // stores the new options internally, but does not rerender anything.
12017 recordOptionOverrides: function(newOptionHash
) {
12020 for (optionName
in newOptionHash
) {
12021 this.dynamicOverrides
[optionName
] = newOptionHash
[optionName
];
12024 this.viewSpecCache
= {}; // the dynamic override invalidates the options in this cache, so just clear it
12025 this.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
12034 defaultAllDayEventDuration
: null,
12035 defaultTimedEventDuration
: null,
12039 initMomentInternals: function() {
12042 this.defaultAllDayEventDuration
= moment
.duration(this.opt('defaultAllDayEventDuration'));
12043 this.defaultTimedEventDuration
= moment
.duration(this.opt('defaultTimedEventDuration'));
12045 // Called immediately, and when any of the options change.
12046 // Happens before any internal objects rebuild or rerender, because this is very core.
12047 this.optionsModel
.watch('buildingMomentLocale', [
12048 '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort',
12049 '?firstDay', '?weekNumberCalculation'
12050 ], function(opts
) {
12051 var weekNumberCalculation
= opts
.weekNumberCalculation
;
12052 var firstDay
= opts
.firstDay
;
12056 if (weekNumberCalculation
=== 'iso') {
12057 weekNumberCalculation
= 'ISO'; // normalize
12060 var localeData
= Object
.create( // make a cheap copy
12061 getMomentLocaleData(opts
.locale
) // will fall back to en
12064 if (opts
.monthNames
) {
12065 localeData
._months
= opts
.monthNames
;
12067 if (opts
.monthNamesShort
) {
12068 localeData
._monthsShort
= opts
.monthNamesShort
;
12070 if (opts
.dayNames
) {
12071 localeData
._weekdays
= opts
.dayNames
;
12073 if (opts
.dayNamesShort
) {
12074 localeData
._weekdaysShort
= opts
.dayNamesShort
;
12077 if (firstDay
== null && weekNumberCalculation
=== 'ISO') {
12080 if (firstDay
!= null) {
12081 _week
= Object
.create(localeData
._week
); // _week: { dow: # }
12082 _week
.dow
= firstDay
;
12083 localeData
._week
= _week
;
12086 if ( // whitelist certain kinds of input
12087 weekNumberCalculation
=== 'ISO' ||
12088 weekNumberCalculation
=== 'local' ||
12089 typeof weekNumberCalculation
=== 'function'
12091 localeData
._fullCalendar_weekCalc
= weekNumberCalculation
; // moment-ext will know what to do with it
12094 _this
.localeData
= localeData
;
12096 // If the internal current date object already exists, move to new locale.
12097 // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
12098 if (_this
.currentDate
) {
12099 _this
.localizeMoment(_this
.currentDate
); // sets to localeData
12105 // Builds a moment using the settings of the current calendar: timezone and locale.
12106 // Accepts anything the vanilla moment() constructor accepts.
12107 moment: function() {
12110 if (this.opt('timezone') === 'local') {
12111 mom
= FC
.moment
.apply(null, arguments
);
12113 // Force the moment to be local, because FC.moment doesn't guarantee it.
12114 if (mom
.hasTime()) { // don't give ambiguously-timed moments a local zone
12118 else if (this.opt('timezone') === 'UTC') {
12119 mom
= FC
.moment
.utc
.apply(null, arguments
); // process as UTC
12122 mom
= FC
.moment
.parseZone
.apply(null, arguments
); // let the input decide the zone
12125 this.localizeMoment(mom
); // TODO
12131 msToMoment: function(ms
, forceAllDay
) {
12132 var mom
= FC
.moment
.utc(ms
); // TODO: optimize by using Date.UTC
12138 mom
= this.applyTimezone(mom
); // may or may not apply locale
12141 this.localizeMoment(mom
);
12147 msToUtcMoment: function(ms
, forceAllDay
) {
12148 var mom
= FC
.moment
.utc(ms
); // TODO: optimize by using Date.UTC
12154 this.localizeMoment(mom
);
12160 // Updates the given moment's locale settings to the current calendar locale settings.
12161 localizeMoment: function(mom
) {
12162 mom
._locale
= this.localeData
;
12166 // Returns a boolean about whether or not the calendar knows how to calculate
12167 // the timezone offset of arbitrary dates in the current timezone.
12168 getIsAmbigTimezone: function() {
12169 return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC';
12173 // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
12174 applyTimezone: function(date
) {
12175 if (!date
.hasTime()) {
12176 return date
.clone();
12179 var zonedDate
= this.moment(date
.toArray());
12180 var timeAdjust
= date
.time() - zonedDate
.time();
12181 var adjustedZonedDate
;
12183 // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
12184 if (timeAdjust
) { // is the time result different than expected?
12185 adjustedZonedDate
= zonedDate
.clone().add(timeAdjust
); // add milliseconds
12186 if (date
.time() - adjustedZonedDate
.time() === 0) { // does it match perfectly now?
12187 zonedDate
= adjustedZonedDate
;
12196 Assumes the footprint is non-open-ended.
12198 footprintToDateProfile: function(componentFootprint
, ignoreEnd
) {
12199 var start
= FC
.moment
.utc(componentFootprint
.unzonedRange
.startMs
);
12203 end
= FC
.moment
.utc(componentFootprint
.unzonedRange
.endMs
);
12206 if (componentFootprint
.isAllDay
) {
12214 start
= this.applyTimezone(start
);
12217 end
= this.applyTimezone(end
);
12221 return new EventDateProfile(start
, end
, this);
12225 // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
12226 // Will return an moment with an ambiguous timezone.
12227 getNow: function() {
12228 var now
= this.opt('now');
12229 if (typeof now
=== 'function') {
12232 return this.moment(now
).stripZone();
12236 // Produces a human-readable string for the given duration.
12237 // Side-effect: changes the locale of the given duration.
12238 humanizeDuration: function(duration
) {
12239 return duration
.locale(this.opt('locale')).humanize();
12244 // Event-Specific Date Utilities. TODO: move
12245 // -----------------------------------------------------------------------------------------------------------------
12248 // Get an event's normalized end date. If not present, calculate it from the defaults.
12249 getEventEnd: function(event
) {
12251 return event
.end
.clone();
12254 return this.getDefaultEventEnd(event
.allDay
, event
.start
);
12259 // Given an event's allDay status and start date, return what its fallback end date should be.
12260 // TODO: rename to computeDefaultEventEnd
12261 getDefaultEventEnd: function(allDay
, zonedStart
) {
12262 var end
= zonedStart
.clone();
12265 end
.stripTime().add(this.defaultAllDayEventDuration
);
12268 end
.add(this.defaultTimedEventDuration
);
12271 if (this.getIsAmbigTimezone()) {
12272 end
.stripZone(); // we don't know what the tzo should be
12284 viewSpecCache
: null, // cache of view definitions (initialized in Calendar.js)
12287 // Gets information about how to create a view. Will use a cache.
12288 getViewSpec: function(viewType
) {
12289 var cache
= this.viewSpecCache
;
12291 return cache
[viewType
] || (cache
[viewType
] = this.buildViewSpec(viewType
));
12295 // Given a duration singular unit, like "week" or "day", finds a matching view spec.
12296 // Preference is given to views that have corresponding buttons.
12297 getUnitViewSpec: function(unit
) {
12302 if ($.inArray(unit
, unitsDesc
) != -1) {
12304 // put views that have buttons first. there will be duplicates, but oh well
12305 viewTypes
= this.header
.getViewsWithButtons(); // TODO: include footer as well?
12306 $.each(FC
.views
, function(viewType
) { // all views
12307 viewTypes
.push(viewType
);
12310 for (i
= 0; i
< viewTypes
.length
; i
++) {
12311 spec
= this.getViewSpec(viewTypes
[i
]);
12313 if (spec
.singleUnit
== unit
) {
12322 // Builds an object with information on how to create a given view
12323 buildViewSpec: function(requestedViewType
) {
12324 var viewOverrides
= this.overrides
.views
|| {};
12325 var specChain
= []; // for the view. lowest to highest priority
12326 var defaultsChain
= []; // for the view. lowest to highest priority
12327 var overridesChain
= []; // for the view. lowest to highest priority
12328 var viewType
= requestedViewType
;
12329 var spec
; // for the view
12330 var overrides
; // for the view
12335 // iterate from the specific view definition to a more general one until we hit an actual View class
12337 spec
= fcViews
[viewType
];
12338 overrides
= viewOverrides
[viewType
];
12339 viewType
= null; // clear. might repopulate for another iteration
12341 if (typeof spec
=== 'function') { // TODO: deprecate
12342 spec
= { 'class': spec
};
12346 specChain
.unshift(spec
);
12347 defaultsChain
.unshift(spec
.defaults
|| {});
12348 durationInput
= durationInput
|| spec
.duration
;
12349 viewType
= viewType
|| spec
.type
;
12353 overridesChain
.unshift(overrides
); // view-specific option hashes have options at zero-level
12354 durationInput
= durationInput
|| overrides
.duration
;
12355 viewType
= viewType
|| overrides
.type
;
12359 spec
= mergeProps(specChain
);
12360 spec
.type
= requestedViewType
;
12361 if (!spec
['class']) {
12365 // fall back to top-level `duration` option
12366 durationInput
= durationInput
||
12367 this.dynamicOverrides
.duration
||
12368 this.overrides
.duration
;
12370 if (durationInput
) {
12371 duration
= moment
.duration(durationInput
);
12373 if (duration
.valueOf()) { // valid?
12375 unit
= computeDurationGreatestUnit(duration
, durationInput
);
12377 spec
.duration
= duration
;
12378 spec
.durationUnit
= unit
;
12380 // view is a single-unit duration, like "week" or "day"
12381 // incorporate options for this. lowest priority
12382 if (duration
.as(unit
) === 1) {
12383 spec
.singleUnit
= unit
;
12384 overridesChain
.unshift(viewOverrides
[unit
] || {});
12389 spec
.defaults
= mergeOptions(defaultsChain
);
12390 spec
.overrides
= mergeOptions(overridesChain
);
12392 this.buildViewSpecOptions(spec
);
12393 this.buildViewSpecButtonText(spec
, requestedViewType
);
12399 // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
12400 buildViewSpecOptions: function(spec
) {
12401 spec
.options
= mergeOptions([ // lowest to highest priority
12402 Calendar
.defaults
, // global defaults
12403 spec
.defaults
, // view's defaults (from ViewSubclass.defaults)
12405 this.localeDefaults
, // locale and dir take precedence over view's defaults!
12406 this.overrides
, // calendar's overrides (options given to constructor)
12407 spec
.overrides
, // view's overrides (view-specific options)
12408 this.dynamicOverrides
// dynamically set via setter. highest precedence
12410 populateInstanceComputableOptions(spec
.options
);
12414 // Computes and assigns a view spec's buttonText-related options
12415 buildViewSpecButtonText: function(spec
, requestedViewType
) {
12417 // given an options object with a possible `buttonText` hash, lookup the buttonText for the
12418 // requested view, falling back to a generic unit entry like "week" or "day"
12419 function queryButtonText(options
) {
12420 var buttonText
= options
.buttonText
|| {};
12421 return buttonText
[requestedViewType
] ||
12422 // view can decide to look up a certain key
12423 (spec
.buttonTextKey
? buttonText
[spec
.buttonTextKey
] : null) ||
12424 // a key like "month"
12425 (spec
.singleUnit
? buttonText
[spec
.singleUnit
] : null);
12428 // highest to lowest priority
12429 spec
.buttonTextOverride
=
12430 queryButtonText(this.dynamicOverrides
) ||
12431 queryButtonText(this.overrides
) || // constructor-specified buttonText lookup hash takes precedence
12432 spec
.overrides
.buttonText
; // `buttonText` for view-specific options is a string
12434 // highest to lowest priority. mirrors buildViewSpecOptions
12435 spec
.buttonTextDefault
=
12436 queryButtonText(this.localeDefaults
) ||
12437 queryButtonText(this.dirDefaults
) ||
12438 spec
.defaults
.buttonText
|| // a single string. from ViewSubclass.defaults
12439 queryButtonText(Calendar
.defaults
) ||
12440 (spec
.duration
? this.humanizeDuration(spec
.duration
) : null) || // like "3 days"
12441 requestedViewType
; // fall back to given view name
12452 suggestedViewHeight
: null,
12453 windowResizeProxy
: null,
12454 ignoreWindowResize
: 0,
12457 render: function() {
12458 if (!this.contentEl
) {
12459 this.initialRender();
12461 else if (this.elementVisible()) {
12462 // mainly for the public API
12469 initialRender: function() {
12475 // event delegation for nav links
12476 el
.on('click.fc', 'a[data-goto]', function(ev
) {
12477 var anchorEl
= $(this);
12478 var gotoOptions
= anchorEl
.data('goto'); // will automatically parse JSON
12479 var date
= _this
.moment(gotoOptions
.date
);
12480 var viewType
= gotoOptions
.type
;
12482 // property like "navLinkDayClick". might be a string or a function
12483 var customAction
= _this
.view
.opt('navLink' + capitaliseFirstLetter(viewType
) + 'Click');
12485 if (typeof customAction
=== 'function') {
12486 customAction(date
, ev
);
12489 if (typeof customAction
=== 'string') {
12490 viewType
= customAction
;
12492 _this
.zoomTo(date
, viewType
);
12496 // called immediately, and upon option change
12497 this.optionsModel
.watch('settingTheme', [ '?theme', '?themeSystem' ], function(opts
) {
12498 var themeClass
= ThemeRegistry
.getThemeClass(opts
.themeSystem
|| opts
.theme
);
12499 var theme
= new themeClass(_this
.optionsModel
);
12500 var widgetClass
= theme
.getClass('widget');
12502 _this
.theme
= theme
;
12505 el
.addClass(widgetClass
);
12508 var widgetClass
= _this
.theme
.getClass('widget');
12510 _this
.theme
= null;
12513 el
.removeClass(widgetClass
);
12517 // called immediately, and upon option change.
12518 // HACK: locale often affects isRTL, so we explicitly listen to that too.
12519 this.optionsModel
.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts
) {
12520 el
.toggleClass('fc-ltr', !opts
.isRTL
);
12521 el
.toggleClass('fc-rtl', opts
.isRTL
);
12524 this.contentEl
= $("<div class='fc-view-container'/>").prependTo(el
);
12526 this.initToolbars();
12527 this.renderHeader();
12528 this.renderFooter();
12529 this.renderView(this.opt('defaultView'));
12531 if (this.opt('handleWindowResize')) {
12533 this.windowResizeProxy
= debounce( // prevents rapid calls
12534 this.windowResize
.bind(this),
12535 this.opt('windowResizeDelay')
12542 destroy: function() {
12545 this.view
.removeElement();
12547 // NOTE: don't null-out this.view in case API methods are called after destroy.
12548 // It is still the "current" view, just not rendered.
12551 this.toolbarsManager
.proxyCall('removeElement');
12552 this.contentEl
.remove();
12553 this.el
.removeClass('fc fc-ltr fc-rtl');
12555 // removes theme-related root className
12556 this.optionsModel
.unwatch('settingTheme');
12558 this.el
.off('.fc'); // unbind nav link handlers
12560 if (this.windowResizeProxy
) {
12561 $(window
).unbind('resize', this.windowResizeProxy
);
12562 this.windowResizeProxy
= null;
12565 GlobalEmitter
.unneeded();
12569 elementVisible: function() {
12570 return this.el
.is(':visible');
12576 // -----------------------------------------------------------------------------------
12579 // Renders a view because of a date change, view-type change, or for the first time.
12580 // If not given a viewType, keep the current view but render different dates.
12581 // Accepts an optional scroll state to restore to.
12582 renderView: function(viewType
, forcedScroll
) {
12584 this.ignoreWindowResize
++;
12586 var needsClearView
= this.view
&& viewType
&& this.view
.type
!== viewType
;
12588 // if viewType is changing, remove the old view's rendering
12589 if (needsClearView
) {
12590 this.freezeContentHeight(); // prevent a scroll jump when view element is removed
12594 // if viewType changed, or the view was never created, create a fresh view
12595 if (!this.view
&& viewType
) {
12597 this.viewsByType
[viewType
] ||
12598 (this.viewsByType
[viewType
] = this.instantiateView(viewType
));
12600 this.view
.setElement(
12601 $("<div class='fc-view fc-" + viewType
+ "-view' />").appendTo(this.contentEl
)
12603 this.toolbarsManager
.proxyCall('activateButton', viewType
);
12608 if (forcedScroll
) {
12609 this.view
.addForcedScroll(forcedScroll
);
12612 if (this.elementVisible()) {
12613 this.view
.setDate(this.currentDate
);
12617 if (needsClearView
) {
12618 this.thawContentHeight();
12621 this.ignoreWindowResize
--;
12625 // Unrenders the current view and reflects this change in the Header.
12626 // Unregsiters the `view`, but does not remove from viewByType hash.
12627 clearView: function() {
12628 this.toolbarsManager
.proxyCall('deactivateButton', this.view
.type
);
12629 this.view
.removeElement();
12634 // Destroys the view, including the view object. Then, re-instantiates it and renders it.
12635 // Maintains the same scroll state.
12636 // TODO: maintain any other user-manipulated state.
12637 reinitView: function() {
12638 this.ignoreWindowResize
++;
12639 this.freezeContentHeight();
12641 var viewType
= this.view
.type
;
12642 var scrollState
= this.view
.queryScroll();
12645 this.renderView(viewType
, scrollState
);
12647 this.thawContentHeight();
12648 this.ignoreWindowResize
--;
12653 // -----------------------------------------------------------------------------------
12656 getSuggestedViewHeight: function() {
12657 if (this.suggestedViewHeight
=== null) {
12660 return this.suggestedViewHeight
;
12664 isHeightAuto: function() {
12665 return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto';
12669 updateSize: function(shouldRecalc
) {
12670 if (this.elementVisible()) {
12672 if (shouldRecalc
) {
12676 this.ignoreWindowResize
++;
12677 this.view
.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
12678 this.ignoreWindowResize
--;
12680 return true; // signal success
12685 calcSize: function() {
12686 if (this.elementVisible()) {
12692 _calcSize: function() { // assumes elementVisible
12693 var contentHeightInput
= this.opt('contentHeight');
12694 var heightInput
= this.opt('height');
12696 if (typeof contentHeightInput
=== 'number') { // exists and not 'auto'
12697 this.suggestedViewHeight
= contentHeightInput
;
12699 else if (typeof contentHeightInput
=== 'function') { // exists and is a function
12700 this.suggestedViewHeight
= contentHeightInput();
12702 else if (typeof heightInput
=== 'number') { // exists and not 'auto'
12703 this.suggestedViewHeight
= heightInput
- this.queryToolbarsHeight();
12705 else if (typeof heightInput
=== 'function') { // exists and is a function
12706 this.suggestedViewHeight
= heightInput() - this.queryToolbarsHeight();
12708 else if (heightInput
=== 'parent') { // set to height of parent element
12709 this.suggestedViewHeight
= this.el
.parent().height() - this.queryToolbarsHeight();
12712 this.suggestedViewHeight
= Math
.round(
12713 this.contentEl
.width() /
12714 Math
.max(this.opt('aspectRatio'), .5)
12720 windowResize: function(ev
) {
12722 !this.ignoreWindowResize
&&
12723 ev
.target
=== window
&& // so we don't process jqui "resize" events that have bubbled up
12724 this.view
.renderUnzonedRange
// view has already been rendered
12726 if (this.updateSize(true)) {
12727 this.publiclyTrigger('windowResize', [ this.view
]);
12733 /* Height "Freezing"
12734 -----------------------------------------------------------------------------*/
12737 freezeContentHeight: function() {
12738 this.contentEl
.css({
12740 height
: this.contentEl
.height(),
12746 thawContentHeight: function() {
12747 this.contentEl
.css({
12762 toolbarsManager
: null,
12765 initToolbars: function() {
12766 this.header
= new Toolbar(this, this.computeHeaderOptions());
12767 this.footer
= new Toolbar(this, this.computeFooterOptions());
12768 this.toolbarsManager
= new Iterator([ this.header
, this.footer
]);
12772 computeHeaderOptions: function() {
12774 extraClasses
: 'fc-header-toolbar',
12775 layout
: this.opt('header')
12780 computeFooterOptions: function() {
12782 extraClasses
: 'fc-footer-toolbar',
12783 layout
: this.opt('footer')
12788 // can be called repeatedly and Header will rerender
12789 renderHeader: function() {
12790 var header
= this.header
;
12792 header
.setToolbarOptions(this.computeHeaderOptions());
12796 this.el
.prepend(header
.el
);
12801 // can be called repeatedly and Footer will rerender
12802 renderFooter: function() {
12803 var footer
= this.footer
;
12805 footer
.setToolbarOptions(this.computeFooterOptions());
12809 this.el
.append(footer
.el
);
12814 setToolbarsTitle: function(title
) {
12815 this.toolbarsManager
.proxyCall('updateTitle', title
);
12819 updateToolbarButtons: function() {
12820 var now
= this.getNow();
12821 var view
= this.view
;
12822 var todayInfo
= view
.buildDateProfile(now
);
12823 var prevInfo
= view
.buildPrevDateProfile(this.currentDate
);
12824 var nextInfo
= view
.buildNextDateProfile(this.currentDate
);
12826 this.toolbarsManager
.proxyCall(
12827 (todayInfo
.isValid
&& !view
.currentUnzonedRange
.containsDate(now
)) ?
12833 this.toolbarsManager
.proxyCall(
12840 this.toolbarsManager
.proxyCall(
12849 queryToolbarsHeight: function() {
12850 return this.toolbarsManager
.items
.reduce(function(accumulator
, toolbar
) {
12851 var toolbarHeight
= toolbar
.el
? toolbar
.el
.outerHeight(true) : 0; // includes margin
12852 return accumulator
+ toolbarHeight
;
12860 var BUSINESS_HOUR_EVENT_DEFAULTS
= {
12863 dow
: [ 1, 2, 3, 4, 5 ], // monday - friday
12864 rendering
: 'inverse-background'
12865 // classNames are defined in businessHoursSegClasses
12870 returns ComponentFootprint[]
12871 `businessHourDef` is optional. Use Calendar's setting if omitted.
12873 Calendar
.prototype.buildCurrentBusinessFootprints = function(wholeDay
) {
12874 return this._buildCurrentBusinessFootprints(wholeDay
, this.opt('businessHours'));
12878 Calendar
.prototype._buildCurrentBusinessFootprints = function(wholeDay
, businessDefInput
) {
12879 var eventPeriod
= this.eventManager
.currentPeriod
;
12880 var businessInstanceGroup
;
12883 businessInstanceGroup
= this.buildBusinessInstanceGroup(
12886 eventPeriod
.unzonedRange
12889 if (businessInstanceGroup
) {
12890 return this.eventInstancesToFootprints( // in Calendar.constraints.js
12891 businessInstanceGroup
.eventInstances
12901 If there are business hours, and they are within range, returns populated EventInstanceGroup.
12902 If there are business hours, but they aren't within range, returns a zero-item EventInstanceGroup.
12903 If there are NOT business hours, returns undefined.
12905 Calendar
.prototype.buildBusinessInstanceGroup = function(wholeDay
, rawComplexDef
, unzonedRange
) {
12906 var eventDefs
= this.buildBusinessDefs(wholeDay
, rawComplexDef
);
12907 var eventInstanceGroup
;
12909 if (eventDefs
.length
) {
12910 eventInstanceGroup
= new EventInstanceGroup(
12911 eventDefsToEventInstances(eventDefs
, unzonedRange
)
12914 // so that inverse-background rendering can happen even when no eventRanges in view
12915 eventInstanceGroup
.explicitEventDef
= eventDefs
[0];
12917 return eventInstanceGroup
;
12922 Calendar
.prototype.buildBusinessDefs = function(wholeDay
, rawComplexDef
) {
12924 var requireDow
= false;
12928 if (rawComplexDef
=== true) {
12929 rawDefs
= [ {} ]; // will get BUSINESS_HOUR_EVENT_DEFAULTS verbatim
12931 else if ($.isPlainObject(rawComplexDef
)) {
12932 rawDefs
= [ rawComplexDef
];
12934 else if ($.isArray(rawComplexDef
)) {
12935 rawDefs
= rawComplexDef
;
12936 requireDow
= true; // every sub-definition NEEDS a day-of-week
12939 for (i
= 0; i
< rawDefs
.length
; i
++) {
12940 if (!requireDow
|| rawDefs
[i
].dow
) {
12942 this.buildBusinessDef(wholeDay
, rawDefs
[i
])
12951 Calendar
.prototype.buildBusinessDef = function(wholeDay
, rawDef
) {
12952 var fullRawDef
= $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS
, rawDef
);
12955 fullRawDef
.start
= null;
12956 fullRawDef
.end
= null;
12959 return RecurringEventDef
.parse(
12961 new EventSource(this) // dummy source
12968 determines if eventInstanceGroup is allowed,
12969 in relation to other EVENTS and business hours.
12971 Calendar
.prototype.isEventInstanceGroupAllowed = function(eventInstanceGroup
) {
12972 var eventDef
= eventInstanceGroup
.getEventDef();
12973 var eventFootprints
= this.eventRangesToEventFootprints(eventInstanceGroup
.getAllEventRanges());
12976 var peerEventInstances
= this.getPeerEventInstances(eventDef
);
12977 var peerEventRanges
= eventInstancesToEventRanges(peerEventInstances
);
12978 var peerEventFootprints
= this.eventRangesToEventFootprints(peerEventRanges
);
12980 var constraintVal
= eventDef
.getConstraint();
12981 var overlapVal
= eventDef
.getOverlap();
12983 var eventAllowFunc
= this.opt('eventAllow');
12985 for (i
= 0; i
< eventFootprints
.length
; i
++) {
12987 !this.isFootprintAllowed(
12988 eventFootprints
[i
].componentFootprint
,
12989 peerEventFootprints
,
12992 eventFootprints
[i
].eventInstance
12999 if (eventAllowFunc
) {
13000 for (i
= 0; i
< eventFootprints
.length
; i
++) {
13003 eventFootprints
[i
].componentFootprint
.toLegacy(this),
13004 eventFootprints
[i
].getEventLegacy()
13016 Calendar
.prototype.getPeerEventInstances = function(eventDef
) {
13017 return this.eventManager
.getEventInstancesWithoutId(eventDef
.id
);
13021 Calendar
.prototype.isSelectionFootprintAllowed = function(componentFootprint
) {
13022 var peerEventInstances
= this.eventManager
.getEventInstances();
13023 var peerEventRanges
= eventInstancesToEventRanges(peerEventInstances
);
13024 var peerEventFootprints
= this.eventRangesToEventFootprints(peerEventRanges
);
13026 var selectAllowFunc
;
13029 this.isFootprintAllowed(
13030 componentFootprint
,
13031 peerEventFootprints
,
13032 this.opt('selectConstraint'),
13033 this.opt('selectOverlap')
13036 selectAllowFunc
= this.opt('selectAllow');
13038 if (selectAllowFunc
) {
13039 return selectAllowFunc(componentFootprint
.toLegacy(this)) !== false;
13050 Calendar
.prototype.isFootprintAllowed = function(
13051 componentFootprint
,
13052 peerEventFootprints
,
13055 subjectEventInstance
// optional
13057 var constraintFootprints
; // ComponentFootprint[]
13058 var overlapEventFootprints
; // EventFootprint[]
13060 if (constraintVal
!= null) {
13061 constraintFootprints
= this.constraintValToFootprints(constraintVal
, componentFootprint
.isAllDay
);
13063 if (!this.isFootprintWithinConstraints(componentFootprint
, constraintFootprints
)) {
13068 overlapEventFootprints
= this.collectOverlapEventFootprints(peerEventFootprints
, componentFootprint
);
13070 if (overlapVal
=== false) {
13071 if (overlapEventFootprints
.length
) {
13075 else if (typeof overlapVal
=== 'function') {
13076 if (!isOverlapsAllowedByFunc(overlapEventFootprints
, overlapVal
, subjectEventInstance
)) {
13081 if (subjectEventInstance
) {
13082 if (!isOverlapEventInstancesAllowed(overlapEventFootprints
, subjectEventInstance
)) {
13092 // ------------------------------------------------------------------------------------------------
13095 Calendar
.prototype.isFootprintWithinConstraints = function(componentFootprint
, constraintFootprints
) {
13098 for (i
= 0; i
< constraintFootprints
.length
; i
++) {
13099 if (this.footprintContainsFootprint(constraintFootprints
[i
], componentFootprint
)) {
13108 Calendar
.prototype.constraintValToFootprints = function(constraintVal
, isAllDay
) {
13109 var eventInstances
;
13111 if (constraintVal
=== 'businessHours') {
13112 return this.buildCurrentBusinessFootprints(isAllDay
);
13114 else if (typeof constraintVal
=== 'object') {
13115 eventInstances
= this.parseEventDefToInstances(constraintVal
); // handles recurring events
13117 if (!eventInstances
) { // invalid input. fallback to parsing footprint directly
13118 return this.parseFootprints(constraintVal
);
13121 return this.eventInstancesToFootprints(eventInstances
);
13124 else if (constraintVal
!= null) { // an ID
13125 eventInstances
= this.eventManager
.getEventInstancesWithId(constraintVal
);
13127 return this.eventInstancesToFootprints(eventInstances
);
13133 Calendar
.prototype.eventInstancesToFootprints = function(eventInstances
) {
13134 return eventFootprintsToComponentFootprints(
13135 this.eventRangesToEventFootprints(
13136 eventInstancesToEventRanges(eventInstances
)
13143 // ------------------------------------------------------------------------------------------------
13146 Calendar
.prototype.collectOverlapEventFootprints = function(peerEventFootprints
, targetFootprint
) {
13147 var overlapEventFootprints
= [];
13150 for (i
= 0; i
< peerEventFootprints
.length
; i
++) {
13152 this.footprintsIntersect(
13154 peerEventFootprints
[i
].componentFootprint
13157 overlapEventFootprints
.push(peerEventFootprints
[i
]);
13161 return overlapEventFootprints
;
13165 // optional subjectEventInstance
13166 function isOverlapsAllowedByFunc(overlapEventFootprints
, overlapFunc
, subjectEventInstance
) {
13169 for (i
= 0; i
< overlapEventFootprints
.length
; i
++) {
13172 overlapEventFootprints
[i
].eventInstance
.toLegacy(),
13173 subjectEventInstance
? subjectEventInstance
.toLegacy() : null
13184 function isOverlapEventInstancesAllowed(overlapEventFootprints
, subjectEventInstance
) {
13185 var subjectLegacyInstance
= subjectEventInstance
.toLegacy();
13187 var overlapEventInstance
;
13188 var overlapEventDef
;
13191 for (i
= 0; i
< overlapEventFootprints
.length
; i
++) {
13192 overlapEventInstance
= overlapEventFootprints
[i
].eventInstance
;
13193 overlapEventDef
= overlapEventInstance
.def
;
13195 // don't need to pass in calendar, because don't want to consider global eventOverlap property,
13196 // because we already considered that earlier in the process.
13197 overlapVal
= overlapEventDef
.getOverlap();
13199 if (overlapVal
=== false) {
13202 else if (typeof overlapVal
=== 'function') {
13205 overlapEventInstance
.toLegacy(),
13206 subjectLegacyInstance
13218 // Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints
13219 // ------------------------------------------------------------------------------------------------
13220 // NOTE: this might seem like repetitive code with the Grid class, however, this code is related to
13221 // constraints whereas the Grid code is related to rendering. Each approach might want to convert
13222 // eventRanges -> eventFootprints in a different way. Regardless, there are opportunities to make
13227 Returns false on invalid input.
13229 Calendar
.prototype.parseEventDefToInstances = function(eventInput
) {
13230 var eventPeriod
= this.eventManager
.currentPeriod
;
13231 var eventDef
= EventDefParser
.parse(eventInput
, new EventSource(this));
13233 if (!eventDef
) { // invalid
13238 return eventDef
.buildInstances(eventPeriod
.unzonedRange
);
13246 Calendar
.prototype.eventRangesToEventFootprints = function(eventRanges
) {
13248 var eventFootprints
= [];
13250 for (i
= 0; i
< eventRanges
.length
; i
++) {
13251 eventFootprints
.push
.apply(eventFootprints
, // append
13252 this.eventRangeToEventFootprints(eventRanges
[i
])
13256 return eventFootprints
;
13261 TODO: somehow more DRY with Grid::eventRangeToEventFootprints
13263 Calendar
.prototype.eventRangeToEventFootprints = function(eventRange
) {
13265 new EventFootprint(
13266 new ComponentFootprint(
13267 eventRange
.unzonedRange
,
13268 eventRange
.eventDef
.isAllDay()
13270 eventRange
.eventDef
,
13271 eventRange
.eventInstance
// might not exist
13278 Parses footprints directly.
13279 Very similar to EventDateProfile::parse :(
13281 Calendar
.prototype.parseFootprints = function(rawInput
) {
13284 if (rawInput
.start
) {
13285 start
= this.moment(rawInput
.start
);
13287 if (!start
.isValid()) {
13292 if (rawInput
.end
) {
13293 end
= this.moment(rawInput
.end
);
13295 if (!end
.isValid()) {
13301 new ComponentFootprint(
13302 new UnzonedRange(start
, end
),
13303 (start
&& !start
.hasTime()) || (end
&& !end
.hasTime()) // isAllDay
13310 // ----------------------------------------------------------------------------------------
13313 Calendar
.prototype.footprintContainsFootprint = function(outerFootprint
, innerFootprint
) {
13314 return outerFootprint
.unzonedRange
.containsRange(innerFootprint
.unzonedRange
);
13318 Calendar
.prototype.footprintsIntersect = function(footprint0
, footprint1
) {
13319 return footprint0
.unzonedRange
.intersectsWith(footprint1
.unzonedRange
);
13327 // ------------------------------------------------------------------------------------
13330 getEventSources: function() {
13331 return this.eventManager
.otherSources
.slice(); // clone
13335 getEventSourceById: function(id
) {
13336 return this.eventManager
.getSourceById(
13337 EventSource
.normalizeId(id
)
13342 addEventSource: function(sourceInput
) {
13343 var source
= EventSourceParser
.parse(sourceInput
, this);
13346 this.eventManager
.addSource(source
);
13351 removeEventSources: function(sourceMultiQuery
) {
13352 var eventManager
= this.eventManager
;
13356 if (sourceMultiQuery
== null) {
13357 this.eventManager
.removeAllSources();
13360 sources
= eventManager
.multiQuerySources(sourceMultiQuery
);
13362 eventManager
.freeze();
13364 for (i
= 0; i
< sources
.length
; i
++) {
13365 eventManager
.removeSource(sources
[i
]);
13368 eventManager
.thaw();
13373 removeEventSource: function(sourceQuery
) {
13374 var eventManager
= this.eventManager
;
13375 var sources
= eventManager
.querySources(sourceQuery
);
13378 eventManager
.freeze();
13380 for (i
= 0; i
< sources
.length
; i
++) {
13381 eventManager
.removeSource(sources
[i
]);
13384 eventManager
.thaw();
13388 refetchEventSources: function(sourceMultiQuery
) {
13389 var eventManager
= this.eventManager
;
13390 var sources
= eventManager
.multiQuerySources(sourceMultiQuery
);
13393 eventManager
.freeze();
13395 for (i
= 0; i
< sources
.length
; i
++) {
13396 eventManager
.refetchSource(sources
[i
]);
13399 eventManager
.thaw();
13404 // ------------------------------------------------------------------------------------
13407 refetchEvents: function() {
13408 this.eventManager
.refetchAllSources();
13412 renderEvents: function(eventInputs
, isSticky
) {
13413 this.eventManager
.freeze();
13415 for (var i
= 0; i
< eventInputs
.length
; i
++) {
13416 this.renderEvent(eventInputs
[i
], isSticky
);
13419 this.eventManager
.thaw();
13423 renderEvent: function(eventInput
, isSticky
) {
13424 var eventManager
= this.eventManager
;
13425 var eventDef
= EventDefParser
.parse(
13427 eventInput
.source
|| eventManager
.stickySource
13431 eventManager
.addEventDef(eventDef
, isSticky
);
13436 // legacyQuery operates on legacy event instance objects
13437 removeEvents: function(legacyQuery
) {
13438 var eventManager
= this.eventManager
;
13439 var eventInstances
= eventManager
.getEventInstances();
13440 var legacyInstances
;
13445 if (legacyQuery
== null) { // shortcut for removing all
13446 eventManager
.removeAllEventDefs();
13449 legacyInstances
= eventInstances
.map(function(eventInstance
) {
13450 return eventInstance
.toLegacy();
13453 legacyInstances
= filterLegacyEventInstances(legacyInstances
, legacyQuery
);
13455 // compute unique IDs
13456 for (i
= 0; i
< legacyInstances
.length
; i
++) {
13457 eventDef
= this.eventManager
.getEventDefByUid(legacyInstances
[i
]._id
);
13458 idMap
[eventDef
.id
] = true;
13461 eventManager
.freeze();
13463 for (i
in idMap
) { // reuse `i` as an "id"
13464 eventManager
.removeEventDefsById(i
);
13467 eventManager
.thaw();
13472 // legacyQuery operates on legacy event instance objects
13473 clientEvents: function(legacyQuery
) {
13474 var eventInstances
= this.eventManager
.getEventInstances();
13475 var legacyEventInstances
= eventInstances
.map(function(eventInstance
) {
13476 return eventInstance
.toLegacy();
13479 return filterLegacyEventInstances(legacyEventInstances
, legacyQuery
);
13483 updateEvents: function(eventPropsArray
) {
13484 this.eventManager
.freeze();
13486 for (var i
= 0; i
< eventPropsArray
.length
; i
++) {
13487 this.updateEvent(eventPropsArray
[i
]);
13490 this.eventManager
.thaw();
13494 updateEvent: function(eventProps
) {
13495 var eventDef
= this.eventManager
.getEventDefByUid(eventProps
._id
);
13497 var eventDefMutation
;
13499 if (eventDef
instanceof SingleEventDef
) {
13500 eventInstance
= eventDef
.buildInstance();
13502 eventDefMutation
= EventDefMutation
.createFromRawProps(
13504 eventProps
, // raw props
13505 null // largeUnit -- who uses it?
13508 this.eventManager
.mutateEventsWithId(eventDef
.id
, eventDefMutation
); // will release
13515 function filterLegacyEventInstances(legacyEventInstances
, legacyQuery
) {
13516 if (legacyQuery
== null) {
13517 return legacyEventInstances
;
13519 else if ($.isFunction(legacyQuery
)) {
13520 return legacyEventInstances
.filter(legacyQuery
);
13522 else { // an event ID
13523 legacyQuery
+= ''; // normalize to string
13525 return legacyEventInstances
.filter(function(legacyEventInstance
) {
13526 // soft comparison because id not be normalized to string
13527 return legacyEventInstance
.id
== legacyQuery
;
13534 Calendar
.defaults
= {
13536 titleRangeSeparator
: ' \u2013 ', // en dash
13537 monthYearFormat
: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
13539 defaultTimedEventDuration
: '02:00:00',
13540 defaultAllDayEventDuration
: { days
: 1 },
13541 forceEventDuration
: false,
13542 nextDayThreshold
: '09:00:00', // 9am
13545 defaultView
: 'month',
13550 right
: 'today prev,next'
13553 weekNumbers
: false,
13555 weekNumberTitle
: 'W',
13556 weekNumberCalculation
: 'local',
13560 //nowIndicator: false,
13562 scrollTime
: '06:00:00',
13563 minTime
: '00:00:00',
13564 maxTime
: '24:00:00',
13565 showNonCurrentDates
: true,
13568 lazyFetching
: true,
13569 startParam
: 'start',
13571 timezoneParam
: 'timezone',
13575 //allDayDefault: undefined,
13582 prevYear
: "prev year",
13583 nextYear
: "next year",
13584 year
: 'year', // TODO: locale files need to specify this
13590 //buttonIcons: null,
13592 allDayText
: 'all-day',
13594 // jquery-ui theming
13596 //themeButtonIcons: null,
13598 //eventResizableFromStart: false,
13600 dragRevertDuration
: 500,
13603 //selectable: false,
13604 unselectAuto
: true,
13605 //selectMinDistance: 0,
13609 eventOrder
: 'title',
13610 //eventRenderWait: null,
13613 eventLimitText
: 'more',
13614 eventLimitClick
: 'popover',
13615 dayPopoverFormat
: 'LL',
13617 handleWindowResize
: true,
13618 windowResizeDelay
: 100, // milliseconds before an updateSize happens
13620 longPressDelay
: 1000
13625 Calendar
.englishDefaults
= { // used by locale.js
13626 dayPopoverFormat
: 'dddd, MMMM D'
13630 Calendar
.rtlDefaults
= { // right-to-left defaults
13631 header
: { // TODO: smarter solution (first/center/last ?)
13632 left
: 'next,prev today',
13637 prev
: 'right-single-arrow',
13638 next
: 'left-single-arrow',
13639 prevYear
: 'right-double-arrow',
13640 nextYear
: 'left-double-arrow'
13642 themeButtonIcons
: {
13643 prev
: 'circle-triangle-e',
13644 next
: 'circle-triangle-w',
13645 nextYear
: 'seek-prev',
13646 prevYear
: 'seek-next'
13652 var localeOptionHash
= FC
.locales
= {}; // initialize and expose
13655 // TODO: document the structure and ordering of a FullCalendar locale file
13658 // Initialize jQuery UI datepicker translations while using some of the translations
13659 // Will set this as the default locales for datepicker.
13660 FC
.datepickerLocale = function(localeCode
, dpLocaleCode
, dpOptions
) {
13662 // get the FullCalendar internal option hash for this locale. create if necessary
13663 var fcOptions
= localeOptionHash
[localeCode
] || (localeOptionHash
[localeCode
] = {});
13665 // transfer some simple options from datepicker to fc
13666 fcOptions
.isRTL
= dpOptions
.isRTL
;
13667 fcOptions
.weekNumberTitle
= dpOptions
.weekHeader
;
13669 // compute some more complex options from datepicker
13670 $.each(dpComputableOptions
, function(name
, func
) {
13671 fcOptions
[name
] = func(dpOptions
);
13674 // is jQuery UI Datepicker is on the page?
13675 if ($.datepicker
) {
13677 // Register the locale data.
13678 // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
13679 // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
13680 // Make an alias so the locale can be referenced either way.
13681 $.datepicker
.regional
[dpLocaleCode
] =
13682 $.datepicker
.regional
[localeCode
] = // alias
13685 // Alias 'en' to the default locale data. Do this every time.
13686 $.datepicker
.regional
.en
= $.datepicker
.regional
[''];
13688 // Set as Datepicker's global defaults.
13689 $.datepicker
.setDefaults(dpOptions
);
13694 // Sets FullCalendar-specific translations. Will set the locales as the global default.
13695 FC
.locale = function(localeCode
, newFcOptions
) {
13699 // get the FullCalendar internal option hash for this locale. create if necessary
13700 fcOptions
= localeOptionHash
[localeCode
] || (localeOptionHash
[localeCode
] = {});
13702 // provided new options for this locales? merge them in
13703 if (newFcOptions
) {
13704 fcOptions
= localeOptionHash
[localeCode
] = mergeOptions([ fcOptions
, newFcOptions
]);
13707 // compute locale options that weren't defined.
13708 // always do this. newFcOptions can be undefined when initializing from i18n file,
13709 // so no way to tell if this is an initialization or a default-setting.
13710 momOptions
= getMomentLocaleData(localeCode
); // will fall back to en
13711 $.each(momComputableOptions
, function(name
, func
) {
13712 if (fcOptions
[name
] == null) {
13713 fcOptions
[name
] = func(momOptions
, fcOptions
);
13717 // set it as the default locale for FullCalendar
13718 Calendar
.defaults
.locale
= localeCode
;
13722 // NOTE: can't guarantee any of these computations will run because not every locale has datepicker
13723 // configs, so make sure there are English fallbacks for these in the defaults file.
13724 var dpComputableOptions
= {
13726 buttonText: function(dpOptions
) {
13728 // the translations sometimes wrongly contain HTML entities
13729 prev
: stripHtmlEntities(dpOptions
.prevText
),
13730 next
: stripHtmlEntities(dpOptions
.nextText
),
13731 today
: stripHtmlEntities(dpOptions
.currentText
)
13735 // Produces format strings like "MMMM YYYY" -> "September 2014"
13736 monthYearFormat: function(dpOptions
) {
13737 return dpOptions
.showMonthAfterYear
?
13738 'YYYY[' + dpOptions
.yearSuffix
+ '] MMMM' :
13739 'MMMM YYYY[' + dpOptions
.yearSuffix
+ ']';
13744 var momComputableOptions
= {
13746 // Produces format strings like "ddd M/D" -> "Fri 9/15"
13747 dayOfMonthFormat: function(momOptions
, fcOptions
) {
13748 var format
= momOptions
.longDateFormat('l'); // for the format like "M/D/YYYY"
13750 // strip the year off the edge, as well as other misc non-whitespace chars
13751 format
= format
.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
13753 if (fcOptions
.isRTL
) {
13754 format
+= ' ddd'; // for RTL, add day-of-week to end
13757 format
= 'ddd ' + format
; // for LTR, add day-of-week to beginning
13762 // Produces format strings like "h:mma" -> "6:00pm"
13763 mediumTimeFormat: function(momOptions
) { // can't be called `timeFormat` because collides with option
13764 return momOptions
.longDateFormat('LT')
13765 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
13768 // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
13769 smallTimeFormat: function(momOptions
) {
13770 return momOptions
.longDateFormat('LT')
13771 .replace(':mm', '(:mm)')
13772 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
13773 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
13776 // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
13777 extraSmallTimeFormat: function(momOptions
) {
13778 return momOptions
.longDateFormat('LT')
13779 .replace(':mm', '(:mm)')
13780 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
13781 .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
13784 // Produces format strings like "ha" / "H" -> "6pm" / "18"
13785 hourFormat: function(momOptions
) {
13786 return momOptions
.longDateFormat('LT')
13787 .replace(':mm', '')
13788 .replace(/(\Wmm)$/, '') // like above, but for foreign locales
13789 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
13792 // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
13793 noMeridiemTimeFormat: function(momOptions
) {
13794 return momOptions
.longDateFormat('LT')
13795 .replace(/\s*a$/i, ''); // remove trailing AM/PM
13801 // options that should be computed off live calendar options (considers override options)
13802 // TODO: best place for this? related to locale?
13803 // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
13804 var instanceComputableOptions
= {
13806 // Produces format strings for results like "Mo 16"
13807 smallDayDateFormat: function(options
) {
13808 return options
.isRTL
?
13813 // Produces format strings for results like "Wk 5"
13814 weekFormat: function(options
) {
13815 return options
.isRTL
?
13816 'w[ ' + options
.weekNumberTitle
+ ']' :
13817 '[' + options
.weekNumberTitle
+ ' ]w';
13820 // Produces format strings for results like "Wk5"
13821 smallWeekFormat: function(options
) {
13822 return options
.isRTL
?
13823 'w[' + options
.weekNumberTitle
+ ']' :
13824 '[' + options
.weekNumberTitle
+ ']w';
13829 // TODO: make these computable properties in optionsModel
13830 function populateInstanceComputableOptions(options
) {
13831 $.each(instanceComputableOptions
, function(name
, func
) {
13832 if (options
[name
] == null) {
13833 options
[name
] = func(options
);
13839 // Returns moment's internal locale data. If doesn't exist, returns English.
13840 function getMomentLocaleData(localeCode
) {
13841 return moment
.localeData(localeCode
) || moment
.localeData('en');
13845 // Initialize English by forcing computation of moment-derived options.
13846 // Also, sets it as the default.
13847 FC
.locale('en', Calendar
.englishDefaults
);
13851 var UnzonedRange
= FC
.UnzonedRange
= Class
.extend({
13853 startMs
: null, // if null, no start constraint
13854 endMs
: null, // if null, no end constraint
13856 // TODO: move these into footprint.
13857 // Especially, doesn't make sense for null startMs/endMs.
13861 constructor: function(startInput
, endInput
) {
13863 if (moment
.isMoment(startInput
)) {
13864 startInput
= startInput
.clone().stripZone();
13867 if (moment
.isMoment(endInput
)) {
13868 endInput
= endInput
.clone().stripZone();
13872 this.startMs
= startInput
.valueOf();
13876 this.endMs
= endInput
.valueOf();
13880 intersect: function(otherRange
) {
13881 var startMs
= this.startMs
;
13882 var endMs
= this.endMs
;
13883 var newRange
= null;
13885 if (otherRange
.startMs
!== null) {
13886 if (startMs
=== null) {
13887 startMs
= otherRange
.startMs
;
13890 startMs
= Math
.max(startMs
, otherRange
.startMs
);
13894 if (otherRange
.endMs
!== null) {
13895 if (endMs
=== null) {
13896 endMs
= otherRange
.endMs
;
13899 endMs
= Math
.min(endMs
, otherRange
.endMs
);
13903 if (startMs
=== null || endMs
=== null || startMs
< endMs
) {
13904 newRange
= new UnzonedRange(startMs
, endMs
);
13905 newRange
.isStart
= this.isStart
&& startMs
=== this.startMs
;
13906 newRange
.isEnd
= this.isEnd
&& endMs
=== this.endMs
;
13913 intersectsWith: function(otherRange
) {
13914 return (this.endMs
=== null || otherRange
.startMs
=== null || this.endMs
> otherRange
.startMs
) &&
13915 (this.startMs
=== null || otherRange
.endMs
=== null || this.startMs
< otherRange
.endMs
);
13919 containsRange: function(innerRange
) {
13920 return (this.startMs
=== null || (innerRange
.startMs
!== null && innerRange
.startMs
>= this.startMs
)) &&
13921 (this.endMs
=== null || (innerRange
.endMs
!== null && innerRange
.endMs
<= this.endMs
));
13925 // `date` can be a moment, a Date, or a millisecond time.
13926 containsDate: function(date
) {
13927 var ms
= date
.valueOf();
13929 return (this.startMs
=== null || ms
>= this.startMs
) &&
13930 (this.endMs
=== null || ms
< this.endMs
);
13934 // If the given date is not within the given range, move it inside.
13935 // (If it's past the end, make it one millisecond before the end).
13936 // `date` can be a moment, a Date, or a millisecond time.
13937 // Returns a MS-time.
13938 constrainDate: function(date
) {
13939 var ms
= date
.valueOf();
13941 if (this.startMs
!== null && ms
< this.startMs
) {
13945 if (this.endMs
!== null && ms
>= this.endMs
) {
13946 ms
= this.endMs
- 1;
13953 equals: function(otherRange
) {
13954 return this.startMs
=== otherRange
.startMs
&& this.endMs
=== otherRange
.endMs
;
13958 clone: function() {
13959 var range
= new UnzonedRange(this.startMs
, this.endMs
);
13961 range
.isStart
= this.isStart
;
13962 range
.isEnd
= this.isEnd
;
13968 // Returns an ambig-zoned moment from startMs.
13969 // BEWARE: returned moment is not localized.
13970 // Formatting and start-of-week will be default.
13971 getStart: function() {
13972 if (this.startMs
!== null) {
13973 return FC
.moment
.utc(this.startMs
).stripZone();
13977 // Returns an ambig-zoned moment from startMs.
13978 // BEWARE: returned moment is not localized.
13979 // Formatting and start-of-week will be default.
13980 getEnd: function() {
13981 if (this.endMs
!== null) {
13982 return FC
.moment
.utc(this.endMs
).stripZone();
13990 SIDEEFFECT: will mutate eventRanges.
13991 Will return a new array result.
13992 Only works for non-open-ended ranges.
13994 function invertUnzonedRanges(ranges
, constraintRange
) {
13995 var invertedRanges
= [];
13996 var startMs
= constraintRange
.startMs
; // the end of the previous range. the start of the new range
14000 // ranges need to be in order. required for our date-walking algorithm
14001 ranges
.sort(compareUnzonedRanges
);
14003 for (i
= 0; i
< ranges
.length
; i
++) {
14004 dateRange
= ranges
[i
];
14006 // add the span of time before the event (if there is any)
14007 if (dateRange
.startMs
> startMs
) { // compare millisecond time (skip any ambig logic)
14008 invertedRanges
.push(
14009 new UnzonedRange(startMs
, dateRange
.startMs
)
14013 if (dateRange
.endMs
> startMs
) {
14014 startMs
= dateRange
.endMs
;
14018 // add the span of time after the last event (if there is any)
14019 if (startMs
< constraintRange
.endMs
) { // compare millisecond time (skip any ambig logic)
14020 invertedRanges
.push(
14021 new UnzonedRange(startMs
, constraintRange
.endMs
)
14025 return invertedRanges
;
14030 Only works for non-open-ended ranges.
14032 function compareUnzonedRanges(range1
, range2
) {
14033 return range1
.startMs
- range2
.startMs
; // earlier ranges go first
14039 Meant to be immutable
14041 var ComponentFootprint
= FC
.ComponentFootprint
= Class
.extend({
14043 unzonedRange
: null,
14044 isAllDay
: false, // component can choose to ignore this
14047 constructor: function(unzonedRange
, isAllDay
) {
14048 this.unzonedRange
= unzonedRange
;
14049 this.isAllDay
= isAllDay
;
14054 Only works for non-open-ended ranges.
14056 toLegacy: function(calendar
) {
14058 start
: calendar
.msToMoment(this.unzonedRange
.startMs
, this.isAllDay
),
14059 end
: calendar
.msToMoment(this.unzonedRange
.endMs
, this.isAllDay
)
14067 var EventManager
= Class
.extend(EmitterMixin
, ListenerMixin
, {
14069 currentPeriod
: null,
14072 stickySource
: null,
14073 otherSources
: null, // does not include sticky source
14076 constructor: function(calendar
) {
14077 this.calendar
= calendar
;
14078 this.stickySource
= new ArrayEventSource(calendar
);
14079 this.otherSources
= [];
14083 requestEvents: function(start
, end
, timezone
, force
) {
14086 !this.currentPeriod
||
14087 !this.currentPeriod
.isWithinRange(start
, end
) ||
14088 timezone
!== this.currentPeriod
.timezone
14090 this.setPeriod( // will change this.currentPeriod
14091 new EventPeriod(start
, end
, timezone
)
14095 return this.currentPeriod
.whenReleased();
14099 // Source Adding/Removing
14100 // -----------------------------------------------------------------------------------------------------------------
14103 addSource: function(eventSource
) {
14104 this.otherSources
.push(eventSource
);
14106 if (this.currentPeriod
) {
14107 this.currentPeriod
.requestSource(eventSource
); // might release
14112 removeSource: function(doomedSource
) {
14113 removeExact(this.otherSources
, doomedSource
);
14115 if (this.currentPeriod
) {
14116 this.currentPeriod
.purgeSource(doomedSource
); // might release
14121 removeAllSources: function() {
14122 this.otherSources
= [];
14124 if (this.currentPeriod
) {
14125 this.currentPeriod
.purgeAllSources(); // might release
14130 // Source Refetching
14131 // -----------------------------------------------------------------------------------------------------------------
14134 refetchSource: function(eventSource
) {
14135 var currentPeriod
= this.currentPeriod
;
14137 if (currentPeriod
) {
14138 currentPeriod
.freeze();
14139 currentPeriod
.purgeSource(eventSource
);
14140 currentPeriod
.requestSource(eventSource
);
14141 currentPeriod
.thaw();
14146 refetchAllSources: function() {
14147 var currentPeriod
= this.currentPeriod
;
14149 if (currentPeriod
) {
14150 currentPeriod
.freeze();
14151 currentPeriod
.purgeAllSources();
14152 currentPeriod
.requestSources(this.getSources());
14153 currentPeriod
.thaw();
14159 // -----------------------------------------------------------------------------------------------------------------
14162 getSources: function() {
14163 return [ this.stickySource
].concat(this.otherSources
);
14167 // like querySources, but accepts multple match criteria (like multiple IDs)
14168 multiQuerySources: function(matchInputs
) {
14170 // coerce into an array
14171 if (!matchInputs
) {
14174 else if (!$.isArray(matchInputs
)) {
14175 matchInputs
= [ matchInputs
];
14178 var matchingSources
= [];
14181 // resolve raw inputs to real event source objects
14182 for (i
= 0; i
< matchInputs
.length
; i
++) {
14183 matchingSources
.push
.apply( // append
14185 this.querySources(matchInputs
[i
])
14189 return matchingSources
;
14193 // matchInput can either by a real event source object, an ID, or the function/URL for the source.
14194 // returns an array of matching source objects.
14195 querySources: function(matchInput
) {
14196 var sources
= this.otherSources
;
14199 // given a proper event source object
14200 for (i
= 0; i
< sources
.length
; i
++) {
14201 source
= sources
[i
];
14203 if (source
=== matchInput
) {
14209 source
= this.getSourceById(EventSource
.normalizeId(matchInput
));
14214 // parse as an event source
14215 matchInput
= EventSourceParser
.parse(matchInput
, this.calendar
);
14218 return $.grep(sources
, function(source
) {
14219 return isSourcesEquivalent(matchInput
, source
);
14226 ID assumed to already be normalized
14228 getSourceById: function(id
) {
14229 return $.grep(this.otherSources
, function(source
) {
14230 return source
.id
&& source
.id
=== id
;
14236 // -----------------------------------------------------------------------------------------------------------------
14239 setPeriod: function(eventPeriod
) {
14240 if (this.currentPeriod
) {
14241 this.unbindPeriod(this.currentPeriod
);
14242 this.currentPeriod
= null;
14245 this.currentPeriod
= eventPeriod
;
14246 this.bindPeriod(eventPeriod
);
14248 eventPeriod
.requestSources(this.getSources());
14252 bindPeriod: function(eventPeriod
) {
14253 this.listenTo(eventPeriod
, 'release', function(eventsPayload
) {
14254 this.trigger('release', eventsPayload
);
14259 unbindPeriod: function(eventPeriod
) {
14260 this.stopListeningTo(eventPeriod
);
14264 // Event Getting/Adding/Removing
14265 // -----------------------------------------------------------------------------------------------------------------
14268 getEventDefByUid: function(uid
) {
14269 if (this.currentPeriod
) {
14270 return this.currentPeriod
.getEventDefByUid(uid
);
14275 addEventDef: function(eventDef
, isSticky
) {
14277 this.stickySource
.addEventDef(eventDef
);
14280 if (this.currentPeriod
) {
14281 this.currentPeriod
.addEventDef(eventDef
); // might release
14286 removeEventDefsById: function(eventId
) {
14287 this.getSources().forEach(function(eventSource
) {
14288 eventSource
.removeEventDefsById(eventId
);
14291 if (this.currentPeriod
) {
14292 this.currentPeriod
.removeEventDefsById(eventId
); // might release
14297 removeAllEventDefs: function() {
14298 this.getSources().forEach(function(eventSource
) {
14299 eventSource
.removeAllEventDefs();
14302 if (this.currentPeriod
) {
14303 this.currentPeriod
.removeAllEventDefs();
14309 // -----------------------------------------------------------------------------------------------------------------
14313 Returns an undo function.
14315 mutateEventsWithId: function(eventDefId
, eventDefMutation
) {
14316 var currentPeriod
= this.currentPeriod
;
14318 var undoFuncs
= [];
14320 if (currentPeriod
) {
14322 currentPeriod
.freeze();
14324 eventDefs
= currentPeriod
.getEventDefsById(eventDefId
);
14325 eventDefs
.forEach(function(eventDef
) {
14326 // add/remove esp because id might change
14327 currentPeriod
.removeEventDef(eventDef
);
14328 undoFuncs
.push(eventDefMutation
.mutateSingle(eventDef
));
14329 currentPeriod
.addEventDef(eventDef
);
14332 currentPeriod
.thaw();
14334 return function() {
14335 currentPeriod
.freeze();
14337 for (var i
= 0; i
< eventDefs
.length
; i
++) {
14338 currentPeriod
.removeEventDef(eventDefs
[i
]);
14340 currentPeriod
.addEventDef(eventDefs
[i
]);
14343 currentPeriod
.thaw();
14347 return function() { };
14352 copies and then mutates
14354 buildMutatedEventInstanceGroup: function(eventDefId
, eventDefMutation
) {
14355 var eventDefs
= this.getEventDefsById(eventDefId
);
14358 var allInstances
= [];
14360 for (i
= 0; i
< eventDefs
.length
; i
++) {
14361 defCopy
= eventDefs
[i
].clone();
14363 if (defCopy
instanceof SingleEventDef
) {
14364 eventDefMutation
.mutateSingle(defCopy
);
14366 allInstances
.push
.apply(allInstances
, // append
14367 defCopy
.buildInstances()
14372 return new EventInstanceGroup(allInstances
);
14377 // -----------------------------------------------------------------------------------------------------------------
14380 freeze: function() {
14381 if (this.currentPeriod
) {
14382 this.currentPeriod
.freeze();
14388 if (this.currentPeriod
) {
14389 this.currentPeriod
.thaw();
14396 // Methods that straight-up query the current EventPeriod for an array of results.
14398 'getEventDefsById',
14399 'getEventInstances',
14400 'getEventInstancesWithId',
14401 'getEventInstancesWithoutId'
14402 ].forEach(function(methodName
) {
14404 EventManager
.prototype[methodName
] = function() {
14405 var currentPeriod
= this.currentPeriod
;
14407 if (currentPeriod
) {
14408 return currentPeriod
[methodName
].apply(currentPeriod
, arguments
);
14416 function isSourcesEquivalent(source0
, source1
) {
14417 return source0
.getPrimitive() == source1
.getPrimitive();
14422 var EventPeriod
= Class
.extend(EmitterMixin
, {
14428 unzonedRange
: null,
14430 requestsByUid
: null,
14434 stuntedReleaseCnt
: 0,
14437 eventDefsByUid
: null,
14438 eventDefsById
: null,
14439 eventInstanceGroupsById
: null,
14442 constructor: function(start
, end
, timezone
) {
14443 this.start
= start
;
14445 this.timezone
= timezone
;
14447 this.unzonedRange
= new UnzonedRange(
14448 start
.clone().stripZone(),
14449 end
.clone().stripZone()
14452 this.requestsByUid
= {};
14453 this.eventDefsByUid
= {};
14454 this.eventDefsById
= {};
14455 this.eventInstanceGroupsById
= {};
14459 isWithinRange: function(start
, end
) {
14460 // TODO: use a range util function?
14461 return !start
.isBefore(this.start
) && !end
.isAfter(this.end
);
14465 // Requesting and Purging
14466 // -----------------------------------------------------------------------------------------------------------------
14469 requestSources: function(sources
) {
14472 for (var i
= 0; i
< sources
.length
; i
++) {
14473 this.requestSource(sources
[i
]);
14480 requestSource: function(source
) {
14482 var request
= { source
: source
, status
: 'pending' };
14484 this.requestsByUid
[source
.uid
] = request
;
14485 this.pendingCnt
+= 1;
14487 source
.fetch(this.start
, this.end
, this.timezone
).then(function(eventDefs
) {
14488 if (request
.status
!== 'cancelled') {
14489 request
.status
= 'completed';
14490 request
.eventDefs
= eventDefs
;
14492 _this
.addEventDefs(eventDefs
);
14493 _this
.pendingCnt
--;
14494 _this
.tryRelease();
14496 }, function() { // failure
14497 if (request
.status
!== 'cancelled') {
14498 request
.status
= 'failed';
14500 _this
.pendingCnt
--;
14501 _this
.tryRelease();
14507 purgeSource: function(source
) {
14508 var request
= this.requestsByUid
[source
.uid
];
14511 delete this.requestsByUid
[source
.uid
];
14513 if (request
.status
=== 'pending') {
14514 request
.status
= 'cancelled';
14518 else if (request
.status
=== 'completed') {
14519 request
.eventDefs
.forEach(this.removeEventDef
.bind(this));
14525 purgeAllSources: function() {
14526 var requestsByUid
= this.requestsByUid
;
14528 var completedCnt
= 0;
14530 for (uid
in requestsByUid
) {
14531 request
= requestsByUid
[uid
];
14533 if (request
.status
=== 'pending') {
14534 request
.status
= 'cancelled';
14536 else if (request
.status
=== 'completed') {
14541 this.requestsByUid
= {};
14542 this.pendingCnt
= 0;
14544 if (completedCnt
) {
14545 this.removeAllEventDefs(); // might release
14550 // Event Definitions
14551 // -----------------------------------------------------------------------------------------------------------------
14554 getEventDefByUid: function(eventDefUid
) {
14555 return this.eventDefsByUid
[eventDefUid
];
14559 getEventDefsById: function(eventDefId
) {
14560 var a
= this.eventDefsById
[eventDefId
];
14563 return a
.slice(); // clone
14570 addEventDefs: function(eventDefs
) {
14571 for (var i
= 0; i
< eventDefs
.length
; i
++) {
14572 this.addEventDef(eventDefs
[i
]);
14577 addEventDef: function(eventDef
) {
14578 var eventDefsById
= this.eventDefsById
;
14579 var eventDefId
= eventDef
.id
;
14580 var eventDefs
= eventDefsById
[eventDefId
] || (eventDefsById
[eventDefId
] = []);
14581 var eventInstances
= eventDef
.buildInstances(this.unzonedRange
);
14584 eventDefs
.push(eventDef
);
14586 this.eventDefsByUid
[eventDef
.uid
] = eventDef
;
14588 for (i
= 0; i
< eventInstances
.length
; i
++) {
14589 this.addEventInstance(eventInstances
[i
], eventDefId
);
14594 removeEventDefsById: function(eventDefId
) {
14597 this.getEventDefsById(eventDefId
).forEach(function(eventDef
) {
14598 _this
.removeEventDef(eventDef
);
14603 removeAllEventDefs: function() {
14604 var isEmpty
= $.isEmptyObject(this.eventDefsByUid
);
14606 this.eventDefsByUid
= {};
14607 this.eventDefsById
= {};
14608 this.eventInstanceGroupsById
= {};
14616 removeEventDef: function(eventDef
) {
14617 var eventDefsById
= this.eventDefsById
;
14618 var eventDefs
= eventDefsById
[eventDef
.id
];
14620 delete this.eventDefsByUid
[eventDef
.uid
];
14623 removeExact(eventDefs
, eventDef
);
14625 if (!eventDefs
.length
) {
14626 delete eventDefsById
[eventDef
.id
];
14629 this.removeEventInstancesForDef(eventDef
);
14635 // -----------------------------------------------------------------------------------------------------------------
14638 getEventInstances: function() { // TODO: consider iterator
14639 var eventInstanceGroupsById
= this.eventInstanceGroupsById
;
14640 var eventInstances
= [];
14643 for (id
in eventInstanceGroupsById
) {
14644 eventInstances
.push
.apply(eventInstances
, // append
14645 eventInstanceGroupsById
[id
].eventInstances
14649 return eventInstances
;
14653 getEventInstancesWithId: function(eventDefId
) {
14654 var eventInstanceGroup
= this.eventInstanceGroupsById
[eventDefId
];
14656 if (eventInstanceGroup
) {
14657 return eventInstanceGroup
.eventInstances
.slice(); // clone
14664 getEventInstancesWithoutId: function(eventDefId
) { // TODO: consider iterator
14665 var eventInstanceGroupsById
= this.eventInstanceGroupsById
;
14666 var matchingInstances
= [];
14669 for (id
in eventInstanceGroupsById
) {
14670 if (id
!== eventDefId
) {
14671 matchingInstances
.push
.apply(matchingInstances
, // append
14672 eventInstanceGroupsById
[id
].eventInstances
14677 return matchingInstances
;
14681 addEventInstance: function(eventInstance
, eventDefId
) {
14682 var eventInstanceGroupsById
= this.eventInstanceGroupsById
;
14683 var eventInstanceGroup
= eventInstanceGroupsById
[eventDefId
] ||
14684 (eventInstanceGroupsById
[eventDefId
] = new EventInstanceGroup());
14686 eventInstanceGroup
.eventInstances
.push(eventInstance
);
14692 removeEventInstancesForDef: function(eventDef
) {
14693 var eventInstanceGroupsById
= this.eventInstanceGroupsById
;
14694 var eventInstanceGroup
= eventInstanceGroupsById
[eventDef
.id
];
14697 if (eventInstanceGroup
) {
14698 removeCnt
= removeMatching(eventInstanceGroup
.eventInstances
, function(currentEventInstance
) {
14699 return currentEventInstance
.def
=== eventDef
;
14702 if (!eventInstanceGroup
.eventInstances
.length
) {
14703 delete eventInstanceGroupsById
[eventDef
.id
];
14713 // Releasing and Freezing
14714 // -----------------------------------------------------------------------------------------------------------------
14717 tryRelease: function() {
14718 if (!this.pendingCnt
) {
14719 if (!this.freezeDepth
) {
14723 this.stuntedReleaseCnt
++;
14729 release: function() {
14731 this.trigger('release', this.eventInstanceGroupsById
);
14735 whenReleased: function() {
14738 if (this.releaseCnt
) {
14739 return Promise
.resolve(this.eventInstanceGroupsById
);
14742 return Promise
.construct(function(onResolve
) {
14743 _this
.one('release', onResolve
);
14749 freeze: function() {
14750 if (!(this.freezeDepth
++)) {
14751 this.stuntedReleaseCnt
= 0;
14757 if (!(--this.freezeDepth
) && this.stuntedReleaseCnt
&& !this.pendingCnt
) {
14766 var EventDefParser
= {
14768 parse: function(eventInput
, source
) {
14770 isTimeString(eventInput
.start
) || moment
.isDuration(eventInput
.start
) ||
14771 isTimeString(eventInput
.end
) || moment
.isDuration(eventInput
.end
)
14773 return RecurringEventDef
.parse(eventInput
, source
);
14776 return SingleEventDef
.parse(eventInput
, source
);
14784 var EventDef
= FC
.EventDef
= Class
.extend(ParsableModelMixin
, {
14786 source
: null, // required
14788 id
: null, // normalized supplied ID
14789 rawId
: null, // unnormalized supplied ID
14790 uid
: null, // internal ID. new ID for every definition
14792 // NOTE: eventOrder sorting relies on these
14799 startEditable
: null,
14800 durationEditable
: null,
14802 backgroundColor
: null,
14806 className
: null, // an array. TODO: rename to className*s* (API breakage)
14810 constructor: function(source
) {
14811 this.source
= source
;
14812 this.className
= [];
14813 this.miscProps
= {};
14817 isAllDay: function() {
14818 // subclasses must implement
14822 buildInstances: function(unzonedRange
) {
14823 // subclasses must implement
14827 clone: function() {
14828 var copy
= new this.constructor(this.source
);
14831 copy
.rawId
= this.rawId
;
14832 copy
.uid
= this.uid
; // not really unique anymore :(
14834 EventDef
.copyVerbatimStandardProps(this, copy
);
14836 copy
.className
= this.className
; // should clone?
14837 copy
.miscProps
= $.extend({}, this.miscProps
);
14843 hasInverseRendering: function() {
14844 return this.getRendering() === 'inverse-background';
14848 hasBgRendering: function() {
14849 var rendering
= this.getRendering();
14851 return rendering
=== 'inverse-background' || rendering
=== 'background';
14855 getRendering: function() {
14856 if (this.rendering
!= null) {
14857 return this.rendering
;
14860 return this.source
.rendering
;
14864 getConstraint: function() {
14865 if (this.constraint
!= null) {
14866 return this.constraint
;
14869 if (this.source
.constraint
!= null) {
14870 return this.source
.constraint
;
14873 return this.source
.calendar
.opt('eventConstraint'); // what about View option?
14877 getOverlap: function() {
14878 if (this.overlap
!= null) {
14879 return this.overlap
;
14882 if (this.source
.overlap
!= null) {
14883 return this.source
.overlap
;
14886 return this.source
.calendar
.opt('eventOverlap'); // what about View option?
14890 isStartExplicitlyEditable: function() {
14891 if (this.startEditable
!== null) {
14892 return this.startEditable
;
14895 return this.source
.startEditable
;
14899 isDurationExplicitlyEditable: function() {
14900 if (this.durationEditable
!== null) {
14901 return this.durationEditable
;
14904 return this.source
.durationEditable
;
14908 isExplicitlyEditable: function() {
14909 if (this.editable
!== null) {
14910 return this.editable
;
14913 return this.source
.editable
;
14917 toLegacy: function() {
14918 var obj
= $.extend({}, this.miscProps
);
14920 obj
._id
= this.uid
;
14921 obj
.source
= this.source
;
14922 obj
.className
= this.className
; // should clone?
14923 obj
.allDay
= this.isAllDay();
14925 if (this.rawId
!= null) {
14926 obj
.id
= this.rawId
;
14929 EventDef
.copyVerbatimStandardProps(this, obj
);
14935 applyManualRawProps: function(rawProps
) {
14937 if (rawProps
.id
!= null) {
14938 this.id
= EventDef
.normalizeId((this.rawId
= rawProps
.id
));
14941 this.id
= EventDef
.generateId();
14944 if (rawProps
._id
!= null) { // accept this prop, even tho somewhat internal
14945 this.uid
= String(rawProps
._id
);
14948 this.uid
= EventDef
.generateId();
14951 // TODO: converge with EventSource
14952 if ($.isArray(rawProps
.className
)) {
14953 this.className
= rawProps
.className
;
14955 if (typeof rawProps
.className
=== 'string') {
14956 this.className
= rawProps
.className
.split(/\s+/);
14963 applyOtherRawProps: function(rawProps
) {
14964 this.miscProps
= rawProps
;
14969 // finish initializing the mixin
14970 EventDef
.allowRawProps
= ParsableModelMixin_allowRawProps
;
14971 EventDef
.copyVerbatimStandardProps
= ParsableModelMixin_copyVerbatimStandardProps
;
14975 // ---------------------------------------------------------------------------------------------------------------------
14976 // TODO: converge with EventSource
14982 EventDef
.normalizeId = function(id
) {
14987 EventDef
.generateId = function() {
14988 return '_fc' + (EventDef
.uuid
++);
14993 // ---------------------------------------------------------------------------------------------------------------------
14996 EventDef
.allowRawProps({
14997 // not automatically assigned (`false`)
15001 source
: false, // will ignored
15003 // automatically assigned (`true`)
15010 startEditable
: true,
15011 durationEditable
: true,
15013 backgroundColor
: true,
15019 EventDef
.parse = function(rawInput
, source
) {
15020 var def
= new this(source
);
15021 var calendarTransform
= source
.calendar
.opt('eventDataTransform');
15022 var sourceTransform
= source
.eventDataTransform
;
15024 if (calendarTransform
) {
15025 rawInput
= calendarTransform(rawInput
);
15027 if (sourceTransform
) {
15028 rawInput
= sourceTransform(rawInput
);
15031 if (def
.applyRawProps(rawInput
)) {
15040 var SingleEventDef
= EventDef
.extend({
15046 Will receive start/end params, but will be ignored.
15048 buildInstances: function() {
15049 return [ this.buildInstance() ];
15053 buildInstance: function() {
15054 return new EventInstance(
15055 this, // definition
15061 isAllDay: function() {
15062 return this.dateProfile
.isAllDay();
15066 clone: function() {
15067 var def
= EventDef
.prototype.clone
.call(this);
15069 def
.dateProfile
= this.dateProfile
;
15075 rezone: function() {
15076 var calendar
= this.source
.calendar
;
15077 var dateProfile
= this.dateProfile
;
15079 this.dateProfile
= new EventDateProfile(
15080 calendar
.moment(dateProfile
.start
),
15081 dateProfile
.end
? calendar
.moment(dateProfile
.end
) : null,
15088 NOTE: if super-method fails, should still attempt to apply
15090 applyManualRawProps: function(rawProps
) {
15091 var superSuccess
= EventDef
.prototype.applyManualRawProps
.apply(this, arguments
);
15092 var dateProfile
= EventDateProfile
.parse(rawProps
, this.source
); // returns null on failure
15095 this.dateProfile
= dateProfile
;
15097 // make sure `date` shows up in the legacy event objects as-is
15098 if (rawProps
.date
!= null) {
15099 this.miscProps
.date
= rawProps
.date
;
15102 return superSuccess
;
15113 // ---------------------------------------------------------------------------------------------------------------------
15116 SingleEventDef
.allowRawProps({ // false = manually process
15118 date
: false, // alias for 'start'
15125 var RecurringEventDef
= EventDef
.extend({
15127 startTime
: null, // duration
15128 endTime
: null, // duration, or null
15129 dowHash
: null, // object hash, or null
15132 isAllDay: function() {
15133 return !this.startTime
&& !this.endTime
;
15137 buildInstances: function(unzonedRange
) {
15138 var calendar
= this.source
.calendar
;
15139 var unzonedDate
= unzonedRange
.getStart();
15140 var unzonedEnd
= unzonedRange
.getEnd();
15142 var instanceStart
, instanceEnd
;
15143 var instances
= [];
15145 while (unzonedDate
.isBefore(unzonedEnd
)) {
15147 // if everyday, or this particular day-of-week
15148 if (!this.dowHash
|| this.dowHash
[unzonedDate
.day()]) {
15150 zonedDayStart
= calendar
.applyTimezone(unzonedDate
);
15151 instanceStart
= zonedDayStart
.clone();
15152 instanceEnd
= null;
15154 if (this.startTime
) {
15155 instanceStart
.time(this.startTime
);
15158 instanceStart
.stripTime();
15161 if (this.endTime
) {
15162 instanceEnd
= zonedDayStart
.clone().time(this.endTime
);
15167 this, // definition
15168 new EventDateProfile(instanceStart
, instanceEnd
, calendar
)
15173 unzonedDate
.add(1, 'days');
15180 setDow: function(dowNumbers
) {
15182 if (!this.dowHash
) {
15186 for (var i
= 0; i
< dowNumbers
.length
; i
++) {
15187 this.dowHash
[dowNumbers
[i
]] = true;
15192 clone: function() {
15193 var def
= EventDef
.prototype.clone
.call(this);
15195 if (def
.startTime
) {
15196 def
.startTime
= moment
.duration(this.startTime
);
15200 def
.endTime
= moment
.duration(this.endTime
);
15203 if (this.dowHash
) {
15204 def
.dowHash
= $.extend({}, this.dowHash
);
15212 NOTE: if super-method fails, should still attempt to apply
15214 applyRawProps: function(rawProps
) {
15215 var superSuccess
= EventDef
.prototype.applyRawProps
.apply(this, arguments
);
15217 if (rawProps
.start
) {
15218 this.startTime
= moment
.duration(rawProps
.start
);
15221 if (rawProps
.end
) {
15222 this.endTime
= moment
.duration(rawProps
.end
);
15225 if (rawProps
.dow
) {
15226 this.setDow(rawProps
.dow
);
15229 return superSuccess
;
15236 // ---------------------------------------------------------------------------------------------------------------------
15239 RecurringEventDef
.allowRawProps({ // false = manually process
15247 var EventInstance
= Class
.extend({
15249 def
: null, // EventDef
15250 dateProfile
: null, // EventDateProfile
15253 constructor: function(def
, dateProfile
) {
15255 this.dateProfile
= dateProfile
;
15259 toLegacy: function() {
15260 var dateProfile
= this.dateProfile
;
15261 var obj
= this.def
.toLegacy();
15263 obj
.start
= dateProfile
.start
.clone();
15264 obj
.end
= dateProfile
.end
? dateProfile
.end
.clone() : null;
15274 It's expected that there will be at least one EventInstance,
15275 OR that an explicitEventDef is assigned.
15277 var EventInstanceGroup
= Class
.extend({
15279 eventInstances
: null,
15280 explicitEventDef
: null, // optional
15283 constructor: function(eventInstances
) {
15284 this.eventInstances
= eventInstances
|| [];
15288 getAllEventRanges: function() {
15289 return eventInstancesToEventRanges(this.eventInstances
);
15293 sliceRenderRanges: function(constraintRange
) {
15294 if (this.isInverse()) {
15295 return this.sliceInverseRenderRanges(constraintRange
);
15298 return this.sliceNormalRenderRanges(constraintRange
);
15303 sliceNormalRenderRanges: function(constraintRange
) {
15304 var eventInstances
= this.eventInstances
;
15305 var i
, eventInstance
;
15307 var slicedEventRanges
= [];
15309 for (i
= 0; i
< eventInstances
.length
; i
++) {
15310 eventInstance
= eventInstances
[i
];
15312 slicedRange
= eventInstance
.dateProfile
.unzonedRange
.intersect(constraintRange
);
15315 slicedEventRanges
.push(
15325 return slicedEventRanges
;
15329 sliceInverseRenderRanges: function(constraintRange
) {
15330 var unzonedRanges
= eventInstancesToUnzonedRanges(this.eventInstances
);
15331 var ownerDef
= this.getEventDef();
15333 unzonedRanges
= invertUnzonedRanges(unzonedRanges
, constraintRange
);
15335 return unzonedRanges
.map(function(unzonedRange
) {
15336 return new EventRange(unzonedRange
, ownerDef
); // don't give an EventDef
15341 isInverse: function() {
15342 return this.getEventDef().hasInverseRendering();
15346 getEventDef: function() {
15347 return this.explicitEventDef
|| this.eventInstances
[0].def
;
15355 Meant to be immutable
15357 var EventDateProfile
= Class
.extend({
15361 unzonedRange
: null,
15364 constructor: function(start
, end
, calendar
) {
15365 this.start
= start
;
15366 this.end
= end
|| null;
15367 this.unzonedRange
= this.buildUnzonedRange(calendar
);
15371 isAllDay: function() {
15372 return !(this.start
.hasTime() || (this.end
&& this.end
.hasTime()));
15377 Needs a Calendar object
15379 buildUnzonedRange: function(calendar
) {
15380 var startMs
= this.start
.clone().stripZone().valueOf();
15381 var endMs
= this.getEnd(calendar
).stripZone().valueOf();
15383 return new UnzonedRange(startMs
, endMs
);
15388 Needs a Calendar object
15390 getEnd: function(calendar
) {
15393 // derive the end from the start and allDay. compute allDay if necessary
15394 calendar
.getDefaultEventEnd(
15404 Needs an EventSource object
15406 EventDateProfile
.parse = function(rawProps
, source
) {
15407 var startInput
= rawProps
.start
|| rawProps
.date
;
15408 var endInput
= rawProps
.end
;
15414 var calendar
= source
.calendar
;
15415 var start
= calendar
.moment(startInput
);
15416 var end
= endInput
? calendar
.moment(endInput
) : null;
15417 var forcedAllDay
= rawProps
.allDay
;
15418 var forceEventDuration
= calendar
.opt('forceEventDuration');
15420 if (!start
.isValid()) {
15424 if (end
&& (!end
.isValid() || !end
.isAfter(start
))) {
15428 if (forcedAllDay
== null) {
15429 forcedAllDay
= source
.allDayDefault
;
15430 if (forcedAllDay
== null) {
15431 forcedAllDay
= calendar
.opt('allDayDefault');
15435 if (forcedAllDay
=== true) {
15441 else if (forcedAllDay
=== false) {
15442 if (!start
.hasTime()) {
15445 if (end
&& !end
.hasTime()) {
15450 if (!end
&& forceEventDuration
) {
15451 end
= calendar
.getDefaultEventEnd(!start
.hasTime(), start
);
15454 return new EventDateProfile(start
, end
, calendar
);
15459 var EventRange
= Class
.extend({
15461 unzonedRange
: null,
15463 eventInstance
: null, // optional
15466 constructor: function(unzonedRange
, eventDef
, eventInstance
) {
15467 this.unzonedRange
= unzonedRange
;
15468 this.eventDef
= eventDef
;
15470 if (eventInstance
) {
15471 this.eventInstance
= eventInstance
;
15479 var EventFootprint
= FC
.EventFootprint
= Class
.extend({
15481 componentFootprint
: null,
15483 eventInstance
: null, // optional
15486 constructor: function(componentFootprint
, eventDef
, eventInstance
) {
15487 this.componentFootprint
= componentFootprint
;
15488 this.eventDef
= eventDef
;
15490 if (eventInstance
) {
15491 this.eventInstance
= eventInstance
;
15496 getEventLegacy: function() {
15497 return (this.eventInstance
|| this.eventDef
).toLegacy();
15504 var EventDefMutation
= FC
.EventDefMutation
= Class
.extend({
15506 // won't ever be empty. will be null instead.
15507 // callers should use setDateMutation for setting.
15508 dateMutation
: null,
15510 // hack to get updateEvent/createFromRawProps to work.
15511 // not undo-able and not considered in isEmpty.
15512 rawProps
: null, // raw (pre-parse-like)
15516 eventDef assumed to be a SingleEventDef.
15517 returns an undo function.
15519 mutateSingle: function(eventDef
) {
15520 var origDateProfile
;
15522 if (this.dateMutation
) {
15523 origDateProfile
= eventDef
.dateProfile
;
15525 eventDef
.dateProfile
= this.dateMutation
.buildNewDateProfile(
15527 eventDef
.source
.calendar
15532 if (this.rawProps
) {
15533 eventDef
.applyRawProps(this.rawProps
);
15536 if (origDateProfile
) {
15537 return function() {
15538 eventDef
.dateProfile
= origDateProfile
;
15542 return function() { };
15547 setDateMutation: function(dateMutation
) {
15548 if (dateMutation
&& !dateMutation
.isEmpty()) {
15549 this.dateMutation
= dateMutation
;
15552 this.dateMutation
= null;
15557 isEmpty: function() {
15558 return !this.dateMutation
;
15564 EventDefMutation
.createFromRawProps = function(eventInstance
, newRawProps
, largeUnit
) {
15565 var eventDef
= eventInstance
.def
;
15566 var applicableRawProps
= {};
15568 var newDateProfile
;
15572 for (propName
in newRawProps
) {
15574 // ignore object-type custom properties and any date-related properties,
15575 // as well as any other internal property
15576 typeof newRawProps
[propName
] !== 'object' &&
15577 propName
!== 'start' && propName
!== 'end' && propName
!== 'allDay' &&
15578 propName
!== 'source' && propName
!== '_id'
15580 applicableRawProps
[propName
] = newRawProps
[propName
];
15584 newDateProfile
= EventDateProfile
.parse(newRawProps
, eventDef
.source
);
15586 if (newDateProfile
) { // no failure?
15587 dateMutation
= EventDefDateMutation
.createFromDiff(
15588 eventInstance
.dateProfile
,
15594 defMutation
= new EventDefMutation();
15595 defMutation
.rawProps
= applicableRawProps
;
15597 if (dateMutation
) {
15598 defMutation
.dateMutation
= dateMutation
;
15601 return defMutation
;
15606 var EventDefDateMutation
= Class
.extend({
15610 forceAllDay
: false,
15612 // Durations. if 0-ms duration, will be null instead.
15613 // Callers should not set this directly.
15620 returns an undo function.
15622 buildNewDateProfile: function(eventDateProfile
, calendar
) {
15623 var start
= eventDateProfile
.start
.clone();
15625 var shouldRezone
= false;
15627 if (!this.clearEnd
&& eventDateProfile
.end
) {
15628 end
= eventDateProfile
.end
.clone();
15631 if (this.forceTimed
) {
15632 shouldRezone
= true;
15634 if (!start
.hasTime()) {
15638 if (end
&& !end
.hasTime()) {
15642 else if (this.forceAllDay
) {
15644 if (start
.hasTime()) {
15648 if (end
&& end
.hasTime()) {
15653 if (this.dateDelta
) {
15654 shouldRezone
= true;
15656 start
.add(this.dateDelta
);
15659 end
.add(this.dateDelta
);
15663 // do this before adding startDelta to start, so we can work off of start
15664 if (this.endDelta
) {
15665 shouldRezone
= true;
15668 end
= calendar
.getDefaultEventEnd(eventDateProfile
.isAllDay(), start
);
15671 end
.add(this.endDelta
);
15674 if (this.startDelta
) {
15675 shouldRezone
= true;
15677 start
.add(this.startDelta
);
15680 if (shouldRezone
) {
15681 start
= calendar
.applyTimezone(start
);
15684 end
= calendar
.applyTimezone(end
);
15688 // TODO: okay to access calendar option?
15689 if (!end
&& calendar
.opt('forceEventDuration')) {
15690 end
= calendar
.getDefaultEventEnd(eventDateProfile
.isAllDay(), start
);
15693 return new EventDateProfile(start
, end
, calendar
);
15697 setDateDelta: function(dateDelta
) {
15698 if (dateDelta
&& dateDelta
.valueOf()) {
15699 this.dateDelta
= dateDelta
;
15702 this.dateDelta
= null;
15707 setStartDelta: function(startDelta
) {
15708 if (startDelta
&& startDelta
.valueOf()) {
15709 this.startDelta
= startDelta
;
15712 this.startDelta
= null;
15717 setEndDelta: function(endDelta
) {
15718 if (endDelta
&& endDelta
.valueOf()) {
15719 this.endDelta
= endDelta
;
15722 this.endDelta
= null;
15727 isEmpty: function() {
15728 return !this.clearEnd
&& !this.forceTimed
&& !this.forceAllDay
&&
15729 !this.dateDelta
&& !this.startDelta
&& !this.endDelta
;
15735 EventDefDateMutation
.createFromDiff = function(dateProfile0
, dateProfile1
, largeUnit
) {
15736 var clearEnd
= dateProfile0
.end
&& !dateProfile1
.end
;
15737 var forceTimed
= dateProfile0
.isAllDay() && !dateProfile1
.isAllDay();
15738 var forceAllDay
= !dateProfile0
.isAllDay() && dateProfile1
.isAllDay();
15744 // subtracts the dates in the appropriate way, returning a duration
15745 function subtractDates(date1
, date0
) { // date1 - date0
15747 return diffByUnit(date1
, date0
, largeUnit
); // poorly named
15749 else if (dateProfile1
.isAllDay()) {
15750 return diffDay(date1
, date0
); // poorly named
15753 return diffDayTime(date1
, date0
); // poorly named
15757 dateDelta
= subtractDates(dateProfile1
.start
, dateProfile0
.start
);
15759 if (dateProfile1
.end
) {
15760 // use unzonedRanges because dateProfile0.end might be null
15761 endDiff
= subtractDates(
15762 dateProfile1
.unzonedRange
.getEnd(),
15763 dateProfile0
.unzonedRange
.getEnd()
15765 endDelta
= endDiff
.subtract(dateDelta
);
15768 mutation
= new EventDefDateMutation();
15769 mutation
.clearEnd
= clearEnd
;
15770 mutation
.forceTimed
= forceTimed
;
15771 mutation
.forceAllDay
= forceAllDay
;
15772 mutation
.setDateDelta(dateDelta
);
15773 mutation
.setEndDelta(endDelta
);
15780 function eventDefsToEventInstances(eventDefs
, unzonedRange
) {
15781 var eventInstances
= [];
15784 for (i
= 0; i
< eventDefs
.length
; i
++) {
15785 eventInstances
.push
.apply(eventInstances
, // append
15786 eventDefs
[i
].buildInstances(unzonedRange
)
15790 return eventInstances
;
15794 function eventInstancesToEventRanges(eventInstances
) {
15795 return eventInstances
.map(function(eventInstance
) {
15796 return new EventRange(
15797 eventInstance
.dateProfile
.unzonedRange
,
15805 function eventInstancesToUnzonedRanges(eventInstances
) {
15806 return eventInstances
.map(function(eventInstance
) {
15807 return eventInstance
.dateProfile
.unzonedRange
;
15812 function eventFootprintsToComponentFootprints(eventFootprints
) {
15813 return eventFootprints
.map(function(eventFootprint
) {
15814 return eventFootprint
.componentFootprint
;
15820 var EventSource
= Class
.extend(ParsableModelMixin
, {
15824 id
: null, // can stay null
15827 backgroundColor
: null,
15830 className
: null, // array
15832 startEditable
: null,
15833 durationEditable
: null,
15837 allDayDefault
: null,
15838 eventDataTransform
: null, // optional function
15841 constructor: function(calendar
) {
15842 this.calendar
= calendar
;
15843 this.className
= [];
15844 this.uid
= String(EventSource
.uuid
++);
15848 fetch: function(start
, end
, timezone
) {
15849 // subclasses must implement. must return a promise.
15853 removeEventDefsById: function(eventDefId
) {
15854 // optional for subclasses to implement
15858 removeAllEventDefs: function() {
15859 // optional for subclasses to implement
15864 For compairing/matching
15866 getPrimitive: function(otherSource
) {
15867 // subclasses must implement
15871 parseEventDefs: function(rawEventDefs
) {
15874 var eventDefs
= [];
15876 for (i
= 0; i
< rawEventDefs
.length
; i
++) {
15877 eventDef
= EventDefParser
.parse(
15883 eventDefs
.push(eventDef
);
15891 applyManualRawProps: function(rawProps
) {
15893 if (rawProps
.id
!= null) {
15894 this.id
= EventSource
.normalizeId(rawProps
.id
);
15897 // TODO: converge with EventDef
15898 if ($.isArray(rawProps
.className
)) {
15899 this.className
= rawProps
.className
;
15901 else if (typeof rawProps
.className
=== 'string') {
15902 this.className
= rawProps
.className
.split(/\s+/);
15911 // finish initializing the mixin
15912 EventSource
.allowRawProps
= ParsableModelMixin_allowRawProps
;
15916 // ---------------------------------------------------------------------------------------------------------------------
15917 // TODO: converge with EventDef
15920 EventSource
.uuid
= 0;
15923 EventSource
.normalizeId = function(id
) {
15933 // ---------------------------------------------------------------------------------------------------------------------
15936 EventSource
.allowRawProps({
15937 // manually process...
15941 // automatically transfer...
15943 backgroundColor
: true,
15947 startEditable
: true,
15948 durationEditable
: true,
15952 allDayDefault
: true,
15953 eventDataTransform
: true
15958 rawInput can be any data type!
15960 EventSource
.parse = function(rawInput
, calendar
) {
15961 var source
= new this(calendar
);
15963 if (typeof rawInput
=== 'object') {
15964 if (source
.applyRawProps(rawInput
)) {
15973 FC
.EventSource
= EventSource
;
15977 var EventSourceParser
= {
15982 registerClass: function(EventSourceClass
) {
15983 this.sourceClasses
.unshift(EventSourceClass
); // give highest priority
15987 parse: function(rawInput
, calendar
) {
15988 var sourceClasses
= this.sourceClasses
;
15992 for (i
= 0; i
< sourceClasses
.length
; i
++) {
15993 eventSource
= sourceClasses
[i
].parse(rawInput
, calendar
);
15996 return eventSource
;
16004 FC
.EventSourceParser
= EventSourceParser
;
16008 var ArrayEventSource
= EventSource
.extend({
16010 rawEventDefs
: null, // unparsed
16012 currentTimezone
: null,
16015 constructor: function(calendar
) {
16016 EventSource
.apply(this, arguments
); // super-constructor
16017 this.eventDefs
= []; // for if setRawEventDefs is never called
16021 setRawEventDefs: function(rawEventDefs
) {
16022 this.rawEventDefs
= rawEventDefs
;
16023 this.eventDefs
= this.parseEventDefs(rawEventDefs
);
16027 fetch: function(start
, end
, timezone
) {
16028 var eventDefs
= this.eventDefs
;
16032 this.currentTimezone
!== null &&
16033 this.currentTimezone
!== timezone
16035 for (i
= 0; i
< eventDefs
.length
; i
++) {
16036 if (eventDefs
[i
] instanceof SingleEventDef
) {
16037 eventDefs
[i
].rezone();
16042 this.currentTimezone
= timezone
;
16044 return Promise
.resolve(eventDefs
);
16048 addEventDef: function(eventDef
) {
16049 this.eventDefs
.push(eventDef
);
16054 eventDefId already normalized to a string
16056 removeEventDefsById: function(eventDefId
) {
16057 return removeMatching(this.eventDefs
, function(eventDef
) {
16058 return eventDef
.id
=== eventDefId
;
16063 removeAllEventDefs: function() {
16064 this.eventDefs
= [];
16068 getPrimitive: function() {
16069 return this.rawEventDefs
;
16073 applyManualRawProps: function(rawProps
) {
16074 var superSuccess
= EventSource
.prototype.applyManualRawProps
.apply(this, arguments
);
16076 this.setRawEventDefs(rawProps
.events
);
16078 return superSuccess
;
16084 ArrayEventSource
.allowRawProps({
16085 events
: false // don't automatically transfer
16089 ArrayEventSource
.parse = function(rawInput
, calendar
) {
16092 // normalize raw input
16093 if ($.isArray(rawInput
.events
)) { // extended form
16094 rawProps
= rawInput
;
16096 else if ($.isArray(rawInput
)) { // short form
16097 rawProps
= { events
: rawInput
};
16101 return EventSource
.parse
.call(this, rawProps
, calendar
);
16108 EventSourceParser
.registerClass(ArrayEventSource
);
16110 FC
.ArrayEventSource
= ArrayEventSource
;
16114 var FuncEventSource
= EventSource
.extend({
16119 fetch: function(start
, end
, timezone
) {
16122 this.calendar
.pushLoading();
16124 return Promise
.construct(function(onResolve
) {
16130 function(rawEventDefs
) {
16131 _this
.calendar
.popLoading();
16133 onResolve(_this
.parseEventDefs(rawEventDefs
));
16140 getPrimitive: function() {
16145 applyManualRawProps: function(rawProps
) {
16146 var superSuccess
= EventSource
.prototype.applyManualRawProps
.apply(this, arguments
);
16148 this.func
= rawProps
.events
;
16150 return superSuccess
;
16156 FuncEventSource
.allowRawProps({
16157 events
: false // don't automatically transfer
16161 FuncEventSource
.parse = function(rawInput
, calendar
) {
16164 // normalize raw input
16165 if ($.isFunction(rawInput
.events
)) { // extended form
16166 rawProps
= rawInput
;
16168 else if ($.isFunction(rawInput
)) { // short form
16169 rawProps
= { events
: rawInput
};
16173 return EventSource
.parse
.call(this, rawProps
, calendar
);
16180 EventSourceParser
.registerClass(FuncEventSource
);
16182 FC
.FuncEventSource
= FuncEventSource
;
16186 var JsonFeedEventSource
= EventSource
.extend({
16188 // these props must all be manually set before calling fetch
16191 timezoneParam
: null,
16192 ajaxSettings
: null,
16195 fetch: function(start
, end
, timezone
) {
16197 var ajaxSettings
= this.ajaxSettings
;
16198 var onSuccess
= ajaxSettings
.success
;
16199 var onError
= ajaxSettings
.error
;
16200 var requestParams
= this.buildRequestParams(start
, end
, timezone
);
16202 // todo: eventually handle the promise's then,
16203 // don't intercept success/error
16204 // tho will be a breaking API change
16206 this.calendar
.pushLoading();
16208 return Promise
.construct(function(onResolve
, onReject
) {
16210 {}, // avoid mutation
16211 JsonFeedEventSource
.AJAX_DEFAULTS
,
16212 ajaxSettings
, // should have a `url`
16214 data
: requestParams
,
16215 success: function(rawEventDefs
) {
16218 _this
.calendar
.popLoading();
16220 if (rawEventDefs
) {
16221 callbackRes
= applyAll(onSuccess
, this, arguments
); // redirect `this`
16223 if ($.isArray(callbackRes
)) {
16224 rawEventDefs
= callbackRes
;
16227 onResolve(_this
.parseEventDefs(rawEventDefs
));
16233 error: function() {
16234 _this
.calendar
.popLoading();
16236 applyAll(onError
, this, arguments
); // redirect `this`
16245 buildRequestParams: function(start
, end
, timezone
) {
16246 var calendar
= this.calendar
;
16247 var ajaxSettings
= this.ajaxSettings
;
16248 var startParam
, endParam
, timezoneParam
;
16249 var customRequestParams
;
16252 startParam
= this.startParam
;
16253 if (startParam
== null) {
16254 startParam
= calendar
.opt('startParam');
16257 endParam
= this.endParam
;
16258 if (endParam
== null) {
16259 endParam
= calendar
.opt('endParam');
16262 timezoneParam
= this.timezoneParam
;
16263 if (timezoneParam
== null) {
16264 timezoneParam
= calendar
.opt('timezoneParam');
16267 // retrieve any outbound GET/POST $.ajax data from the options
16268 if ($.isFunction(ajaxSettings
.data
)) {
16269 // supplied as a function that returns a key/value object
16270 customRequestParams
= ajaxSettings
.data();
16273 // probably supplied as a straight key/value object
16274 customRequestParams
= ajaxSettings
.data
|| {};
16277 $.extend(params
, customRequestParams
);
16279 params
[startParam
] = start
.format();
16280 params
[endParam
] = end
.format();
16282 if (timezone
&& timezone
!== 'local') {
16283 params
[timezoneParam
] = timezone
;
16290 getPrimitive: function() {
16291 return this.ajaxSettings
.url
;
16295 applyOtherRawProps: function(rawProps
) {
16296 EventSource
.prototype.applyOtherRawProps
.apply(this, arguments
);
16298 this.ajaxSettings
= rawProps
;
16304 JsonFeedEventSource
.AJAX_DEFAULTS
= {
16310 JsonFeedEventSource
.allowRawProps({
16311 // automatically transfer (true)...
16314 timezoneParam
: true
16318 JsonFeedEventSource
.parse = function(rawInput
, calendar
) {
16321 // normalize raw input
16322 if (typeof rawInput
.url
=== 'string') { // extended form
16323 rawProps
= rawInput
;
16325 else if (typeof rawInput
=== 'string') { // short form
16326 rawProps
= { url
: rawInput
}; // will end up in ajaxSettings
16330 return EventSource
.parse
.call(this, rawProps
, calendar
);
16337 EventSourceParser
.registerClass(JsonFeedEventSource
);
16339 FC
.JsonFeedEventSource
= JsonFeedEventSource
;
16343 var ThemeRegistry
= FC
.ThemeRegistry
= {
16345 themeClassHash
: {},
16348 register: function(themeName
, themeClass
) {
16349 this.themeClassHash
[themeName
] = themeClass
;
16353 getThemeClass: function(themeSetting
) {
16354 if (!themeSetting
) {
16355 return StandardTheme
;
16357 else if (themeSetting
=== true) {
16358 return JqueryUiTheme
;
16361 return this.themeClassHash
[themeSetting
];
16369 var Theme
= FC
.Theme
= Class
.extend({
16374 iconOverrideOption
: null,
16375 iconOverrideCustomButtonOption
: null,
16376 iconOverridePrefix
: '',
16379 constructor: function(optionsModel
) {
16380 this.optionsModel
= optionsModel
;
16381 this.processIconOverride();
16385 processIconOverride: function() {
16386 if (this.iconOverrideOption
) {
16387 this.setIconOverride(
16388 this.optionsModel
.get(this.iconOverrideOption
)
16394 setIconOverride: function(iconOverrideHash
) {
16395 var iconClassesCopy
;
16398 if ($.isPlainObject(iconOverrideHash
)) {
16399 iconClassesCopy
= $.extend({}, this.iconClasses
);
16401 for (buttonName
in iconOverrideHash
) {
16402 iconClassesCopy
[buttonName
] = this.applyIconOverridePrefix(
16403 iconOverrideHash
[buttonName
]
16407 this.iconClasses
= iconClassesCopy
;
16409 else if (iconOverrideHash
=== false) {
16410 this.iconClasses
= {};
16415 applyIconOverridePrefix: function(className
) {
16416 var prefix
= this.iconOverridePrefix
;
16418 if (prefix
&& className
.indexOf(prefix
) !== 0) { // if not already present
16419 className
= prefix
+ className
;
16426 getClass: function(key
) {
16427 return this.classes
[key
] || '';
16431 getIconClass: function(buttonName
) {
16432 var className
= this.iconClasses
[buttonName
];
16435 return this.baseIconClass
+ ' ' + className
;
16442 getCustomButtonIconClass: function(customButtonProps
) {
16445 if (this.iconOverrideCustomButtonOption
) {
16446 className
= customButtonProps
[this.iconOverrideCustomButtonOption
];
16449 return this.baseIconClass
+ ' ' + this.applyIconOverridePrefix(className
);
16460 var StandardTheme
= Theme
.extend({
16463 widget
: 'fc-unthemed',
16464 widgetHeader
: 'fc-widget-header',
16465 widgetContent
: 'fc-widget-content',
16467 buttonGroup
: 'fc-button-group',
16468 button
: 'fc-button',
16469 cornerLeft
: 'fc-corner-left',
16470 cornerRight
: 'fc-corner-right',
16471 stateDefault
: 'fc-state-default',
16472 stateActive
: 'fc-state-active',
16473 stateDisabled
: 'fc-state-disabled',
16474 stateHover
: 'fc-state-hover',
16475 stateDown
: 'fc-state-down',
16477 popoverHeader
: 'fc-widget-header',
16478 popoverContent
: 'fc-widget-content',
16481 headerRow
: 'fc-widget-header',
16482 dayRow
: 'fc-widget-content',
16485 listView
: 'fc-widget-content'
16488 baseIconClass
: 'fc-icon',
16490 close
: 'fc-icon-x',
16491 prev
: 'fc-icon-left-single-arrow',
16492 next
: 'fc-icon-right-single-arrow',
16493 prevYear
: 'fc-icon-left-double-arrow',
16494 nextYear
: 'fc-icon-right-double-arrow'
16497 iconOverrideOption
: 'buttonIcons',
16498 iconOverrideCustomButtonOption
: 'icon',
16499 iconOverridePrefix
: 'fc-icon-'
16503 ThemeRegistry
.register('standard', StandardTheme
);
16507 var JqueryUiTheme
= Theme
.extend({
16510 widget
: 'ui-widget',
16511 widgetHeader
: 'ui-widget-header',
16512 widgetContent
: 'ui-widget-content',
16514 buttonGroup
: 'fc-button-group',
16515 button
: 'ui-button',
16516 cornerLeft
: 'ui-corner-left',
16517 cornerRight
: 'ui-corner-right',
16518 stateDefault
: 'ui-state-default',
16519 stateActive
: 'ui-state-active',
16520 stateDisabled
: 'ui-state-disabled',
16521 stateHover
: 'ui-state-hover',
16522 stateDown
: 'ui-state-down',
16524 today
: 'ui-state-highlight',
16526 popoverHeader
: 'ui-widget-header',
16527 popoverContent
: 'ui-widget-content',
16530 headerRow
: 'ui-widget-header',
16531 dayRow
: 'ui-widget-content',
16534 listView
: 'ui-widget-content'
16537 baseIconClass
: 'ui-icon',
16539 close
: 'ui-icon-closethick',
16540 prev
: 'ui-icon-circle-triangle-w',
16541 next
: 'ui-icon-circle-triangle-e',
16542 prevYear
: 'ui-icon-seek-prev',
16543 nextYear
: 'ui-icon-seek-next'
16546 iconOverrideOption
: 'themeButtonIcons',
16547 iconOverrideCustomButtonOption
: 'themeIcon',
16548 iconOverridePrefix
: 'ui-icon-'
16552 ThemeRegistry
.register('jquery-ui', JqueryUiTheme
);
16556 var BootstrapTheme
= Theme
.extend({
16559 widget
: 'fc-bootstrap3',
16561 tableGrid
: 'table-bordered', // avoid `table` class b/c don't want margins. only border color
16562 tableList
: 'table table-striped', // `table` class creates bottom margin but who cares
16564 buttonGroup
: 'btn-group',
16565 button
: 'btn btn-default',
16566 stateActive
: 'active',
16567 stateDisabled
: 'disabled',
16569 today
: 'alert alert-info', // the plain `info` class requires `.table`, too much to ask
16571 popover
: 'panel panel-default',
16572 popoverHeader
: 'panel-heading',
16573 popoverContent
: 'panel-body',
16576 headerRow
: 'panel-default', // avoid `panel` class b/c don't want margins/radius. only border color
16577 dayRow
: 'panel-default', // "
16580 listView
: 'panel panel-default'
16583 baseIconClass
: 'glyphicon',
16585 close
: 'glyphicon-remove',
16586 prev
: 'glyphicon-chevron-left',
16587 next
: 'glyphicon-chevron-right',
16588 prevYear
: 'glyphicon-backward',
16589 nextYear
: 'glyphicon-forward'
16592 iconOverrideOption
: 'bootstrapGlyphicons',
16593 iconOverrideCustomButtonOption
: 'bootstrapGlyphicon',
16594 iconOverridePrefix
: 'glyphicon-'
16598 ThemeRegistry
.register('bootstrap3', BootstrapTheme
);
16602 /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
16603 ----------------------------------------------------------------------------------------------------------------------*/
16604 // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
16605 // It is responsible for managing width/height.
16607 var BasicView
= FC
.BasicView
= View
.extend({
16611 dayGridClass
: DayGrid
, // class the dayGrid will be instantiated from (overridable by subclasses)
16612 dayGrid
: null, // the main subcomponent that does most of the heavy lifting
16614 dayNumbersVisible
: false, // display day numbers on each day cell?
16615 colWeekNumbersVisible
: false, // display week numbers along the side?
16616 cellWeekNumbersVisible
: false, // display week numbers in day cell?
16618 weekNumberWidth
: null, // width of all the week-number cells running down the side
16620 headContainerEl
: null, // div that hold's the dayGrid's rendered date header
16621 headRowEl
: null, // the fake row element of the day-of-week header
16624 initialize: function() {
16625 this.dayGrid
= this.instantiateDayGrid();
16626 this.addChild(this.dayGrid
);
16628 this.scroller
= new Scroller({
16629 overflowX
: 'hidden',
16635 // Generates the DayGrid object this view needs. Draws from this.dayGridClass
16636 instantiateDayGrid: function() {
16637 // generate a subclass on the fly with BasicView-specific behavior
16638 // TODO: cache this subclass
16639 var subclass
= this.dayGridClass
.extend(basicDayGridMethods
);
16641 return new subclass(this);
16645 // Computes the date range that will be rendered.
16646 buildRenderRange: function(currentUnzonedRange
, currentRangeUnit
) {
16647 var renderUnzonedRange
= View
.prototype.buildRenderRange
.apply(this, arguments
); // an UnzonedRange
16648 var start
= this.calendar
.msToUtcMoment(renderUnzonedRange
.startMs
, this.isRangeAllDay
);
16649 var end
= this.calendar
.msToUtcMoment(renderUnzonedRange
.endMs
, this.isRangeAllDay
);
16651 // year and month views should be aligned with weeks. this is already done for week
16652 if (/^(year|month)$/.test(currentRangeUnit
)) {
16653 start
.startOf('week');
16655 // make end-of-week if not already
16656 if (end
.weekday()) {
16657 end
.add(1, 'week').startOf('week'); // exclusively move backwards
16661 return this.trimHiddenDays(new UnzonedRange(start
, end
));
16665 // Renders the view into `this.el`, which should already be assigned
16666 renderDates: function() {
16668 this.dayGrid
.breakOnWeeks
= /year|month|week/.test(this.currentRangeUnit
); // do before Grid::setRange
16669 this.dayGrid
.setRange(this.renderUnzonedRange
);
16671 this.dayNumbersVisible
= this.dayGrid
.rowCnt
> 1; // TODO: make grid responsible
16672 if (this.opt('weekNumbers')) {
16673 if (this.opt('weekNumbersWithinDays')) {
16674 this.cellWeekNumbersVisible
= true;
16675 this.colWeekNumbersVisible
= false;
16678 this.cellWeekNumbersVisible
= false;
16679 this.colWeekNumbersVisible
= true;
16682 this.dayGrid
.numbersVisible
= this.dayNumbersVisible
||
16683 this.cellWeekNumbersVisible
|| this.colWeekNumbersVisible
;
16685 this.el
.addClass('fc-basic-view').html(this.renderSkeletonHtml());
16688 this.scroller
.render();
16689 var dayGridContainerEl
= this.scroller
.el
.addClass('fc-day-grid-container');
16690 var dayGridEl
= $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl
);
16691 this.el
.find('.fc-body > tr > td').append(dayGridContainerEl
);
16693 this.dayGrid
.setElement(dayGridEl
);
16694 this.dayGrid
.renderDates(this.hasRigidRows());
16698 // render the day-of-week headers
16699 renderHead: function() {
16700 this.headContainerEl
=
16701 this.el
.find('.fc-head-container')
16702 .html(this.dayGrid
.renderHeadHtml());
16703 this.headRowEl
= this.headContainerEl
.find('.fc-row');
16707 // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
16708 // always completely kill the dayGrid's rendering.
16709 unrenderDates: function() {
16710 this.dayGrid
.unrenderDates();
16711 this.dayGrid
.removeElement();
16712 this.scroller
.destroy();
16716 // Builds the HTML skeleton for the view.
16717 // The day-grid component will render inside of a container defined by this HTML.
16718 renderSkeletonHtml: function() {
16719 var theme
= this.calendar
.theme
;
16722 '<table class="' + theme
.getClass('tableGrid') + '">' +
16723 '<thead class="fc-head">' +
16725 '<td class="fc-head-container ' + theme
.getClass('widgetHeader') + '"></td>' +
16728 '<tbody class="fc-body">' +
16730 '<td class="' + theme
.getClass('widgetContent') + '"></td>' +
16737 // Generates an HTML attribute string for setting the width of the week number column, if it is known
16738 weekNumberStyleAttr: function() {
16739 if (this.weekNumberWidth
!== null) {
16740 return 'style="width:' + this.weekNumberWidth
+ 'px"';
16746 // Determines whether each row should have a constant height
16747 hasRigidRows: function() {
16748 var eventLimit
= this.opt('eventLimit');
16749 return eventLimit
&& typeof eventLimit
!== 'number';
16754 ------------------------------------------------------------------------------------------------------------------*/
16757 // Refreshes the horizontal dimensions of the view
16758 updateWidth: function() {
16759 if (this.colWeekNumbersVisible
) {
16760 // Make sure all week number cells running down the side have the same width.
16761 // Record the width for cells created later.
16762 this.weekNumberWidth
= matchCellWidths(
16763 this.el
.find('.fc-week-number')
16769 // Adjusts the vertical dimensions of the view to the specified values
16770 setHeight: function(totalHeight
, isAuto
) {
16771 var eventLimit
= this.opt('eventLimit');
16772 var scrollerHeight
;
16773 var scrollbarWidths
;
16775 // reset all heights to be natural
16776 this.scroller
.clear();
16777 uncompensateScroll(this.headRowEl
);
16779 this.dayGrid
.removeSegPopover(); // kill the "more" popover if displayed
16781 // is the event limit a constant level number?
16782 if (eventLimit
&& typeof eventLimit
=== 'number') {
16783 this.dayGrid
.limitRows(eventLimit
); // limit the levels first so the height can redistribute after
16786 // distribute the height to the rows
16787 // (totalHeight is a "recommended" value if isAuto)
16788 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
16789 this.setGridHeight(scrollerHeight
, isAuto
);
16791 // is the event limit dynamically calculated?
16792 if (eventLimit
&& typeof eventLimit
!== 'number') {
16793 this.dayGrid
.limitRows(eventLimit
); // limit the levels after the grid's row heights have been set
16796 if (!isAuto
) { // should we force dimensions of the scroll container?
16798 this.scroller
.setHeight(scrollerHeight
);
16799 scrollbarWidths
= this.scroller
.getScrollbarWidths();
16801 if (scrollbarWidths
.left
|| scrollbarWidths
.right
) { // using scrollbars?
16803 compensateScroll(this.headRowEl
, scrollbarWidths
);
16805 // doing the scrollbar compensation might have created text overflow which created more height. redo
16806 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
16807 this.scroller
.setHeight(scrollerHeight
);
16810 // guarantees the same scrollbar widths
16811 this.scroller
.lockOverflow(scrollbarWidths
);
16816 // given a desired total height of the view, returns what the height of the scroller should be
16817 computeScrollerHeight: function(totalHeight
) {
16818 return totalHeight
-
16819 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
16823 // Sets the height of just the DayGrid component in this view
16824 setGridHeight: function(height
, isAuto
) {
16826 undistributeHeight(this.dayGrid
.rowEls
); // let the rows be their natural height with no expanding
16829 distributeHeight(this.dayGrid
.rowEls
, height
, true); // true = compensate for height-hogging rows
16835 ------------------------------------------------------------------------------------------------------------------*/
16838 computeInitialDateScroll: function() {
16843 queryDateScroll: function() {
16844 return { top
: this.scroller
.getScrollTop() };
16848 applyDateScroll: function(scroll
) {
16849 if (scroll
.top
!== undefined) {
16850 this.scroller
.setScrollTop(scroll
.top
);
16856 ------------------------------------------------------------------------------------------------------------------*/
16859 // Renders the given events onto the view and populates the segments array
16860 renderEventsPayload: function(eventsPayload
) {
16861 this.dayGrid
.renderEventsPayload(eventsPayload
);
16863 // must compensate for events that overflow the row
16864 // TODO: how will ChronoComponent handle this?
16865 this.updateHeight();
16871 // Methods that will customize the rendering behavior of the BasicView's dayGrid
16872 var basicDayGridMethods
= {
16875 // Generates the HTML that will go before the day-of week header cells
16876 renderHeadIntroHtml: function() {
16877 var view
= this.view
;
16879 if (view
.colWeekNumbersVisible
) {
16881 '<th class="fc-week-number ' + view
.calendar
.theme
.getClass('widgetHeader') + '" ' + view
.weekNumberStyleAttr() + '>' +
16882 '<span>' + // needed for matchCellWidths
16883 htmlEscape(this.opt('weekNumberTitle')) +
16892 // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
16893 renderNumberIntroHtml: function(row
) {
16894 var view
= this.view
;
16895 var weekStart
= this.getCellDate(row
, 0);
16897 if (view
.colWeekNumbersVisible
) {
16899 '<td class="fc-week-number" ' + view
.weekNumberStyleAttr() + '>' +
16900 view
.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
16901 { date
: weekStart
, type
: 'week', forceOff
: this.colCnt
=== 1 },
16902 weekStart
.format('w') // inner HTML
16911 // Generates the HTML that goes before the day bg cells for each day-row
16912 renderBgIntroHtml: function() {
16913 var view
= this.view
;
16915 if (view
.colWeekNumbersVisible
) {
16916 return '<td class="fc-week-number ' + view
.calendar
.theme
.getClass('widgetContent') + '" ' +
16917 view
.weekNumberStyleAttr() + '></td>';
16924 // Generates the HTML that goes before every other type of row generated by DayGrid.
16925 // Affects helper-skeleton and highlight-skeleton rows.
16926 renderIntroHtml: function() {
16927 var view
= this.view
;
16929 if (view
.colWeekNumbersVisible
) {
16930 return '<td class="fc-week-number" ' + view
.weekNumberStyleAttr() + '></td>';
16940 /* A month view with day cells running in rows (one-per-week) and columns
16941 ----------------------------------------------------------------------------------------------------------------------*/
16943 var MonthView
= FC
.MonthView
= BasicView
.extend({
16946 // Computes the date range that will be rendered.
16947 buildRenderRange: function() {
16948 var renderUnzonedRange
= BasicView
.prototype.buildRenderRange
.apply(this, arguments
);
16949 var start
= this.calendar
.msToUtcMoment(renderUnzonedRange
.startMs
, this.isRangeAllDay
);
16950 var end
= this.calendar
.msToUtcMoment(renderUnzonedRange
.endMs
, this.isRangeAllDay
);
16954 if (this.isFixedWeeks()) {
16955 rowCnt
= Math
.ceil( // could be partial weeks due to hiddenDays
16956 end
.diff(start
, 'weeks', true) // dontRound=true
16958 end
.add(6 - rowCnt
, 'weeks');
16961 return new UnzonedRange(start
, end
);
16965 // Overrides the default BasicView behavior to have special multi-week auto-height logic
16966 setGridHeight: function(height
, isAuto
) {
16968 // if auto, make the height of each row the height that it would be if there were 6 weeks
16970 height
*= this.rowCnt
/ 6;
16973 distributeHeight(this.dayGrid
.rowEls
, height
, !isAuto
); // if auto, don't compensate for height-hogging rows
16977 isFixedWeeks: function() {
16978 return this.opt('fixedWeekCount');
16982 isDateInOtherMonth: function(date
) {
16983 return date
.month() !== moment
.utc(this.currentUnzonedRange
.startMs
).month(); // TODO: optimize
16994 fcViews
.basicDay
= {
16996 duration
: { days
: 1 }
16999 fcViews
.basicWeek
= {
17001 duration
: { weeks
: 1 }
17005 'class': MonthView
,
17006 duration
: { months
: 1 }, // important for prev/next
17008 fixedWeekCount
: true
17013 /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
17014 ----------------------------------------------------------------------------------------------------------------------*/
17015 // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
17016 // Responsible for managing width/height.
17018 var AgendaView
= FC
.AgendaView
= View
.extend({
17022 timeGridClass
: TimeGrid
, // class used to instantiate the timeGrid. subclasses can override
17023 timeGrid
: null, // the main time-grid subcomponent of this view
17025 dayGridClass
: DayGrid
, // class used to instantiate the dayGrid. subclasses can override
17026 dayGrid
: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
17028 axisWidth
: null, // the width of the time axis running down the side
17030 headContainerEl
: null, // div that hold's the timeGrid's rendered date header
17031 noScrollRowEls
: null, // set of fake row elements that must compensate when scroller has scrollbars
17033 // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
17034 bottomRuleEl
: null,
17036 // indicates that minTime/maxTime affects rendering
17037 usesMinMaxTime
: true,
17040 initialize: function() {
17041 this.timeGrid
= this.instantiateTimeGrid();
17042 this.addChild(this.timeGrid
);
17044 if (this.opt('allDaySlot')) { // should we display the "all-day" area?
17045 this.dayGrid
= this.instantiateDayGrid(); // the all-day subcomponent of this view
17046 this.addChild(this.dayGrid
);
17049 this.scroller
= new Scroller({
17050 overflowX
: 'hidden',
17056 // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
17057 instantiateTimeGrid: function() {
17058 var subclass
= this.timeGridClass
.extend(agendaTimeGridMethods
);
17060 return new subclass(this);
17064 // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
17065 instantiateDayGrid: function() {
17066 var subclass
= this.dayGridClass
.extend(agendaDayGridMethods
);
17068 return new subclass(this);
17073 ------------------------------------------------------------------------------------------------------------------*/
17076 // Renders the view into `this.el`, which has already been assigned
17077 renderDates: function() {
17079 this.timeGrid
.setRange(this.renderUnzonedRange
);
17081 if (this.dayGrid
) {
17082 this.dayGrid
.setRange(this.renderUnzonedRange
);
17085 this.el
.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
17088 this.scroller
.render();
17089 var timeGridWrapEl
= this.scroller
.el
.addClass('fc-time-grid-container');
17090 var timeGridEl
= $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl
);
17091 this.el
.find('.fc-body > tr > td').append(timeGridWrapEl
);
17093 this.timeGrid
.setElement(timeGridEl
);
17094 this.timeGrid
.renderDates();
17096 // the <hr> that sometimes displays under the time-grid
17097 this.bottomRuleEl
= $('<hr class="fc-divider ' + this.calendar
.theme
.getClass('widgetHeader') + '"/>')
17098 .appendTo(this.timeGrid
.el
); // inject it into the time-grid
17100 if (this.dayGrid
) {
17101 this.dayGrid
.setElement(this.el
.find('.fc-day-grid'));
17102 this.dayGrid
.renderDates();
17104 // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
17105 this.dayGrid
.bottomCoordPadding
= this.dayGrid
.el
.next('hr').outerHeight();
17108 this.noScrollRowEls
= this.el
.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
17112 // render the day-of-week headers
17113 renderHead: function() {
17114 this.headContainerEl
=
17115 this.el
.find('.fc-head-container')
17116 .html(this.timeGrid
.renderHeadHtml());
17120 // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
17121 // always completely kill each grid's rendering.
17122 // TODO: move this over to ChronoComponent
17123 unrenderDates: function() {
17124 this.timeGrid
.unrenderDates();
17125 this.timeGrid
.removeElement();
17127 if (this.dayGrid
) {
17128 this.dayGrid
.unrenderDates();
17129 this.dayGrid
.removeElement();
17132 this.scroller
.destroy();
17136 // Builds the HTML skeleton for the view.
17137 // The day-grid and time-grid components will render inside containers defined by this HTML.
17138 renderSkeletonHtml: function() {
17139 var theme
= this.calendar
.theme
;
17142 '<table class="' + theme
.getClass('tableGrid') + '">' +
17143 '<thead class="fc-head">' +
17145 '<td class="fc-head-container ' + theme
.getClass('widgetHeader') + '"></td>' +
17148 '<tbody class="fc-body">' +
17150 '<td class="' + theme
.getClass('widgetContent') + '">' +
17152 '<div class="fc-day-grid"/>' +
17153 '<hr class="fc-divider ' + theme
.getClass('widgetHeader') + '"/>' :
17163 // Generates an HTML attribute string for setting the width of the axis, if it is known
17164 axisStyleAttr: function() {
17165 if (this.axisWidth
!== null) {
17166 return 'style="width:' + this.axisWidth
+ 'px"';
17173 ------------------------------------------------------------------------------------------------------------------*/
17176 getNowIndicatorUnit: function() {
17177 return this.timeGrid
.getNowIndicatorUnit();
17182 ------------------------------------------------------------------------------------------------------------------*/
17185 updateSize: function(isResize
) {
17186 this.timeGrid
.updateSize(isResize
);
17188 View
.prototype.updateSize
.call(this, isResize
); // call the super-method
17192 // Refreshes the horizontal dimensions of the view
17193 updateWidth: function() {
17194 // make all axis cells line up, and record the width so newly created axis cells will have it
17195 this.axisWidth
= matchCellWidths(this.el
.find('.fc-axis'));
17199 // Adjusts the vertical dimensions of the view to the specified values
17200 setHeight: function(totalHeight
, isAuto
) {
17202 var scrollerHeight
;
17203 var scrollbarWidths
;
17205 // reset all dimensions back to the original state
17206 this.bottomRuleEl
.hide(); // .show() will be called later if this <hr> is necessary
17207 this.scroller
.clear(); // sets height to 'auto' and clears overflow
17208 uncompensateScroll(this.noScrollRowEls
);
17210 // limit number of events in the all-day area
17211 if (this.dayGrid
) {
17212 this.dayGrid
.removeSegPopover(); // kill the "more" popover if displayed
17214 eventLimit
= this.opt('eventLimit');
17215 if (eventLimit
&& typeof eventLimit
!== 'number') {
17216 eventLimit
= AGENDA_ALL_DAY_EVENT_LIMIT
; // make sure "auto" goes to a real number
17219 this.dayGrid
.limitRows(eventLimit
);
17223 if (!isAuto
) { // should we force dimensions of the scroll container?
17225 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
17226 this.scroller
.setHeight(scrollerHeight
);
17227 scrollbarWidths
= this.scroller
.getScrollbarWidths();
17229 if (scrollbarWidths
.left
|| scrollbarWidths
.right
) { // using scrollbars?
17231 // make the all-day and header rows lines up
17232 compensateScroll(this.noScrollRowEls
, scrollbarWidths
);
17234 // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
17235 // and reapply the desired height to the scroller.
17236 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
17237 this.scroller
.setHeight(scrollerHeight
);
17240 // guarantees the same scrollbar widths
17241 this.scroller
.lockOverflow(scrollbarWidths
);
17243 // if there's any space below the slats, show the horizontal rule.
17244 // this won't cause any new overflow, because lockOverflow already called.
17245 if (this.timeGrid
.getTotalSlatHeight() < scrollerHeight
) {
17246 this.bottomRuleEl
.show();
17252 // given a desired total height of the view, returns what the height of the scroller should be
17253 computeScrollerHeight: function(totalHeight
) {
17254 return totalHeight
-
17255 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
17260 ------------------------------------------------------------------------------------------------------------------*/
17263 // Computes the initial pre-configured scroll state prior to allowing the user to change it
17264 computeInitialDateScroll: function() {
17265 var scrollTime
= moment
.duration(this.opt('scrollTime'));
17266 var top
= this.timeGrid
.computeTimeTop(scrollTime
);
17268 // zoom can give weird floating-point values. rather scroll a little bit further
17269 top
= Math
.ceil(top
);
17272 top
++; // to overcome top border that slots beyond the first have. looks better
17275 return { top
: top
};
17279 queryDateScroll: function() {
17280 return { top
: this.scroller
.getScrollTop() };
17284 applyDateScroll: function(scroll
) {
17285 if (scroll
.top
!== undefined) {
17286 this.scroller
.setScrollTop(scroll
.top
);
17292 ------------------------------------------------------------------------------------------------------------------*/
17293 // forward all hit-related method calls to the grids (dayGrid might not be defined)
17296 getHitFootprint: function(hit
) {
17297 // TODO: hit.component is set as a hack to identify where the hit came from
17298 return hit
.component
.getHitFootprint(hit
);
17302 getHitEl: function(hit
) {
17303 // TODO: hit.component is set as a hack to identify where the hit came from
17304 return hit
.component
.getHitEl(hit
);
17309 ------------------------------------------------------------------------------------------------------------------*/
17312 // Renders events onto the view and populates the View's segment array
17313 renderEventsPayload: function(eventsPayload
) {
17314 var dayEventsPayload
= {};
17315 var timedEventsPayload
= {};
17318 var id
, eventInstanceGroup
;
17320 // separate the events into all-day and timed
17321 for (id
in eventsPayload
) {
17322 eventInstanceGroup
= eventsPayload
[id
];
17324 if (eventInstanceGroup
.getEventDef().isAllDay()) {
17325 dayEventsPayload
[id
] = eventInstanceGroup
;
17328 timedEventsPayload
[id
] = eventInstanceGroup
;
17332 // render the events in the subcomponents
17333 timedSegs
= this.timeGrid
.renderEventsPayload(timedEventsPayload
);
17334 if (this.dayGrid
) {
17335 daySegs
= this.dayGrid
.renderEventsPayload(dayEventsPayload
);
17338 // the all-day area is flexible and might have a lot of events, so shift the height
17339 // TODO: how will ChronoComponent handle this?
17340 this.updateHeight();
17344 /* Dragging (for events and external elements)
17345 ------------------------------------------------------------------------------------------------------------------*/
17348 // A returned value of `true` signals that a mock "helper" event has been rendered.
17349 renderDrag: function(eventFootprints
, seg
) {
17350 if (eventFootprints
.length
) {
17351 if (!eventFootprints
[0].componentFootprint
.isAllDay
) {
17352 return this.timeGrid
.renderDrag(eventFootprints
, seg
);
17354 else if (this.dayGrid
) {
17355 return this.dayGrid
.renderDrag(eventFootprints
, seg
);
17362 ------------------------------------------------------------------------------------------------------------------*/
17365 // Renders a visual indication of a selection
17366 renderSelectionFootprint: function(componentFootprint
) {
17367 if (!componentFootprint
.isAllDay
) {
17368 this.timeGrid
.renderSelectionFootprint(componentFootprint
);
17370 else if (this.dayGrid
) {
17371 this.dayGrid
.renderSelectionFootprint(componentFootprint
);
17378 // Methods that will customize the rendering behavior of the AgendaView's timeGrid
17379 // TODO: move into TimeGrid
17380 var agendaTimeGridMethods
= {
17383 // Generates the HTML that will go before the day-of week header cells
17384 renderHeadIntroHtml: function() {
17385 var view
= this.view
;
17386 var weekStart
= view
.calendar
.msToUtcMoment(this.unzonedRange
.startMs
, true);
17389 if (this.opt('weekNumbers')) {
17390 weekText
= weekStart
.format(this.opt('smallWeekFormat'));
17393 '<th class="fc-axis fc-week-number ' + view
.calendar
.theme
.getClass('widgetHeader') + '" ' + view
.axisStyleAttr() + '>' +
17394 view
.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
17395 { date
: weekStart
, type
: 'week', forceOff
: this.colCnt
> 1 },
17396 htmlEscape(weekText
) // inner HTML
17401 return '<th class="fc-axis ' + view
.calendar
.theme
.getClass('widgetHeader') + '" ' + view
.axisStyleAttr() + '></th>';
17406 // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
17407 renderBgIntroHtml: function() {
17408 var view
= this.view
;
17410 return '<td class="fc-axis ' + view
.calendar
.theme
.getClass('widgetContent') + '" ' + view
.axisStyleAttr() + '></td>';
17414 // Generates the HTML that goes before all other types of cells.
17415 // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
17416 renderIntroHtml: function() {
17417 var view
= this.view
;
17419 return '<td class="fc-axis" ' + view
.axisStyleAttr() + '></td>';
17425 // Methods that will customize the rendering behavior of the AgendaView's dayGrid
17426 var agendaDayGridMethods
= {
17429 // Generates the HTML that goes before the all-day cells
17430 renderBgIntroHtml: function() {
17431 var view
= this.view
;
17434 '<td class="fc-axis ' + view
.calendar
.theme
.getClass('widgetContent') + '" ' + view
.axisStyleAttr() + '>' +
17435 '<span>' + // needed for matchCellWidths
17436 view
.getAllDayHtml() +
17442 // Generates the HTML that goes before all other types of cells.
17443 // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
17444 renderIntroHtml: function() {
17445 var view
= this.view
;
17447 return '<td class="fc-axis" ' + view
.axisStyleAttr() + '></td>';
17454 var AGENDA_ALL_DAY_EVENT_LIMIT
= 5;
17456 // potential nice values for the slot-duration and interval-duration
17457 // from largest to smallest
17458 var AGENDA_STOCK_SUB_DURATIONS
= [
17467 'class': AgendaView
,
17470 slotDuration
: '00:30:00',
17471 slotEventOverlap
: true // a bad name. confused with overlap/constraint system
17475 fcViews
.agendaDay
= {
17477 duration
: { days
: 1 }
17480 fcViews
.agendaWeek
= {
17482 duration
: { weeks
: 1 }
17487 Responsible for the scroller, and forwarding event-related actions into the "grid"
17489 var ListView
= View
.extend({
17494 initialize: function() {
17495 this.grid
= new ListViewGrid(this);
17496 this.addChild(this.grid
);
17498 this.scroller
= new Scroller({
17499 overflowX
: 'hidden',
17504 renderSkeleton: function() {
17507 this.calendar
.theme
.getClass('listView')
17510 this.scroller
.render();
17511 this.scroller
.el
.appendTo(this.el
);
17513 this.grid
.setElement(this.scroller
.scrollEl
);
17516 unrenderSkeleton: function() {
17517 this.scroller
.destroy(); // will remove the Grid too
17520 setHeight: function(totalHeight
, isAuto
) {
17521 this.scroller
.setHeight(this.computeScrollerHeight(totalHeight
));
17524 computeScrollerHeight: function(totalHeight
) {
17525 return totalHeight
-
17526 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
17529 renderDates: function() {
17530 this.grid
.setRange(this.renderUnzonedRange
); // needs to process range-related options
17533 isEventDefResizable: function(eventDef
) {
17537 isEventDefDraggable: function(eventDef
) {
17544 Responsible for event rendering and user-interaction.
17545 Its "el" is the inner-content of the above view's scroller.
17547 var ListViewGrid
= Grid
.extend({
17549 dayDates
: null, // localized ambig-time moment array
17550 dayRanges
: null, // UnzonedRange[], of start-end of each day
17551 segSelector
: '.fc-list-item', // which elements accept event actions
17552 hasDayInteractions
: false, // no day selection or day clicking
17554 rangeUpdated: function() {
17555 var calendar
= this.view
.calendar
;
17556 var dayStart
= calendar
.msToUtcMoment(this.unzonedRange
.startMs
, true);
17557 var viewEnd
= calendar
.msToUtcMoment(this.unzonedRange
.endMs
, true);
17559 var dayRanges
= [];
17561 while (dayStart
< viewEnd
) {
17563 dayDates
.push(dayStart
.clone());
17565 dayRanges
.push(new UnzonedRange(
17567 dayStart
.clone().add(1, 'day')
17570 dayStart
.add(1, 'day');
17573 this.dayDates
= dayDates
;
17574 this.dayRanges
= dayRanges
;
17578 componentFootprintToSegs: function(footprint
) {
17579 var view
= this.view
;
17580 var dayRanges
= this.dayRanges
;
17586 for (dayIndex
= 0; dayIndex
< dayRanges
.length
; dayIndex
++) {
17587 segRange
= footprint
.unzonedRange
.intersect(dayRanges
[dayIndex
]);
17591 startMs
: segRange
.startMs
,
17592 endMs
: segRange
.endMs
,
17593 isStart
: segRange
.isStart
,
17594 isEnd
: segRange
.isEnd
,
17600 // detect when footprint won't go fully into the next day,
17601 // and mutate the latest seg to the be the end.
17603 !seg
.isEnd
&& !footprint
.isAllDay
&&
17604 footprint
.unzonedRange
.endMs
< dayRanges
[dayIndex
+ 1].startMs
+ view
.nextDayThreshold
17606 seg
.endMs
= footprint
.unzonedRange
.endMs
;
17617 computeEventTimeFormat: function() {
17618 return this.opt('mediumTimeFormat');
17621 // for events with a url, the whole <tr> should be clickable,
17622 // but it's impossible to wrap with an <a> tag. simulate this.
17623 handleSegClick: function(seg
, ev
) {
17626 Grid
.prototype.handleSegClick
.apply(this, arguments
); // super. might prevent the default action
17628 // not clicking on or within an <a> with an href
17629 if (!$(ev
.target
).closest('a[href]').length
) {
17630 url
= seg
.footprint
.eventDef
.url
;
17632 if (url
&& !ev
.isDefaultPrevented()) { // jsEvent not cancelled in handler
17633 window
.location
.href
= url
; // simulate link click
17638 // returns list of foreground segs that were actually rendered
17639 renderFgSegs: function(segs
) {
17640 segs
= this.renderFgSegEls(segs
); // might filter away hidden events
17642 if (!segs
.length
) {
17643 this.renderEmptyMessage();
17646 this.renderSegList(segs
);
17652 renderEmptyMessage: function() {
17654 '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
17655 '<div class="fc-list-empty-wrap1">' +
17656 '<div class="fc-list-empty">' +
17657 htmlEscape(this.opt('noEventsMessage')) +
17664 // render the event segments in the view
17665 renderSegList: function(allSegs
) {
17666 var segsByDay
= this.groupSegsByDay(allSegs
); // sparse array
17670 var tableEl
= $('<table class="fc-list-table ' + this.view
.calendar
.theme
.getClass('tableList') + '"><tbody/></table>');
17671 var tbodyEl
= tableEl
.find('tbody');
17673 for (dayIndex
= 0; dayIndex
< segsByDay
.length
; dayIndex
++) {
17674 daySegs
= segsByDay
[dayIndex
];
17676 if (daySegs
) { // sparse array, so might be undefined
17678 // append a day header
17679 tbodyEl
.append(this.dayHeaderHtml(this.dayDates
[dayIndex
]));
17681 this.sortEventSegs(daySegs
);
17683 for (i
= 0; i
< daySegs
.length
; i
++) {
17684 tbodyEl
.append(daySegs
[i
].el
); // append event row
17689 this.el
.empty().append(tableEl
);
17692 // Returns a sparse array of arrays, segs grouped by their dayIndex
17693 groupSegsByDay: function(segs
) {
17694 var segsByDay
= []; // sparse array
17697 for (i
= 0; i
< segs
.length
; i
++) {
17699 (segsByDay
[seg
.dayIndex
] || (segsByDay
[seg
.dayIndex
] = []))
17706 // generates the HTML for the day headers that live amongst the event rows
17707 dayHeaderHtml: function(dayDate
) {
17708 var view
= this.view
;
17709 var mainFormat
= this.opt('listDayFormat');
17710 var altFormat
= this.opt('listDayAltFormat');
17712 return '<tr class="fc-list-heading" data-date="' + dayDate
.format('YYYY-MM-DD') + '">' +
17713 '<td class="' + view
.calendar
.theme
.getClass('widgetHeader') + '" colspan="3">' +
17715 view
.buildGotoAnchorHtml(
17717 { 'class': 'fc-list-heading-main' },
17718 htmlEscape(dayDate
.format(mainFormat
)) // inner HTML
17722 view
.buildGotoAnchorHtml(
17724 { 'class': 'fc-list-heading-alt' },
17725 htmlEscape(dayDate
.format(altFormat
)) // inner HTML
17732 // generates the HTML for a single event row
17733 fgSegHtml: function(seg
) {
17734 var view
= this.view
;
17735 var calendar
= view
.calendar
;
17736 var theme
= calendar
.theme
;
17737 var classes
= [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg
));
17738 var bgColor
= this.getSegBackgroundColor(seg
);
17739 var eventFootprint
= seg
.footprint
;
17740 var eventDef
= eventFootprint
.eventDef
;
17741 var componentFootprint
= eventFootprint
.componentFootprint
;
17742 var url
= eventDef
.url
;
17745 if (componentFootprint
.isAllDay
) {
17746 timeHtml
= view
.getAllDayHtml();
17748 // if the event appears to span more than one day
17749 else if (view
.isMultiDayRange(componentFootprint
.unzonedRange
)) {
17750 if (seg
.isStart
|| seg
.isEnd
) { // outer segment that probably lasts part of the day
17751 timeHtml
= htmlEscape(this._getEventTimeText(
17752 calendar
.msToMoment(seg
.startMs
),
17753 calendar
.msToMoment(seg
.endMs
),
17754 componentFootprint
.isAllDay
17757 else { // inner segment that lasts the whole day
17758 timeHtml
= view
.getAllDayHtml();
17762 // Display the normal time text for the *event's* times
17763 timeHtml
= htmlEscape(this.getEventTimeText(eventFootprint
));
17767 classes
.push('fc-has-url');
17770 return '<tr class="' + classes
.join(' ') + '">' +
17771 (this.displayEventTime
?
17772 '<td class="fc-list-item-time ' + theme
.getClass('widgetContent') + '">' +
17776 '<td class="fc-list-item-marker ' + theme
.getClass('widgetContent') + '">' +
17777 '<span class="fc-event-dot"' +
17779 ' style="background-color:' + bgColor
+ '"' :
17783 '<td class="fc-list-item-title ' + theme
.getClass('widgetContent') + '">' +
17784 '<a' + (url
? ' href="' + htmlEscape(url
) + '"' : '') + '>' +
17785 htmlEscape(eventDef
.title
|| '') +
17797 buttonTextKey
: 'list', // what to lookup in locale files
17799 buttonText
: 'list', // text to display for English
17800 listDayFormat
: 'LL', // like "January 1, 2016"
17801 noEventsMessage
: 'No events to display'
17805 fcViews
.listDay
= {
17807 duration
: { days
: 1 },
17809 listDayFormat
: 'dddd' // day-of-week is all we need. full date is probably in header
17813 fcViews
.listWeek
= {
17815 duration
: { weeks
: 1 },
17817 listDayFormat
: 'dddd', // day-of-week is more important
17818 listDayAltFormat
: 'LL'
17822 fcViews
.listMonth
= {
17824 duration
: { month
: 1 },
17826 listDayAltFormat
: 'dddd' // day-of-week is nice-to-have
17830 fcViews
.listYear
= {
17832 duration
: { year
: 1 },
17834 listDayAltFormat
: 'dddd' // day-of-week is nice-to-have
17840 return FC
; // export for Node/CommonJS