3 * Docs & License: http://arshaw.com/fullcalendar/
8 if (typeof define
=== 'function' && define
.amd
) {
9 define([ 'jquery', 'moment' ], factory
);
12 factory(jQuery
, moment
);
14 })(function($, moment
) {
18 titleRangeSeparator
: ' \u2014 ', // emphasized dash
19 monthYearFormat
: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
21 defaultTimedEventDuration
: '02:00:00',
22 defaultAllDayEventDuration
: { days
: 1 },
23 forceEventDuration
: false,
24 nextDayThreshold
: '09:00:00', // 9am
32 right
: 'today prev,next'
38 weekNumberCalculation
: 'local',
46 timezoneParam
: 'timezone',
50 //allDayDefault: undefined,
57 prevYear
: "prev year",
58 nextYear
: "next year",
66 prev
: 'left-single-arrow',
67 next
: 'right-single-arrow',
68 prevYear
: 'left-double-arrow',
69 nextYear
: 'right-double-arrow'
75 prev
: 'circle-triangle-w',
76 next
: 'circle-triangle-e',
77 prevYear
: 'seek-prev',
82 dragRevertDuration
: 500,
91 eventLimitText
: 'more',
92 eventLimitClick
: 'popover',
93 dayPopoverFormat
: 'LL',
95 handleWindowResize
: true,
96 windowResizeDelay
: 200 // milliseconds before an updateSize happens
101 var englishDefaults
= {
102 dayPopoverFormat
: 'dddd, MMMM D'
106 // right-to-left defaults
109 left
: 'next,prev today',
114 prev
: 'right-single-arrow',
115 next
: 'left-single-arrow',
116 prevYear
: 'right-double-arrow',
117 nextYear
: 'left-double-arrow'
120 prev
: 'circle-triangle-e',
121 next
: 'circle-triangle-w',
122 nextYear
: 'seek-prev',
123 prevYear
: 'seek-next'
127 var fc
= $.fullCalendar
= { version
: "2.2.5" };
128 var fcViews
= fc
.views
= {};
131 $.fn
.fullCalendar = function(options
) {
132 var args
= Array
.prototype.slice
.call(arguments
, 1); // for a possible method call
133 var res
= this; // what this function will return (this jQuery object by default)
135 this.each(function(i
, _element
) { // loop each DOM element involved
136 var element
= $(_element
);
137 var calendar
= element
.data('fullCalendar'); // get the existing calendar object (if any)
138 var singleRes
; // the returned value of this single method call
141 if (typeof options
=== 'string') {
142 if (calendar
&& $.isFunction(calendar
[options
])) {
143 singleRes
= calendar
[options
].apply(calendar
, args
);
145 res
= singleRes
; // record the first method call result
147 if (options
=== 'destroy') { // for the destroy method, must remove Calendar object data
148 element
.removeData('fullCalendar');
152 // a new calendar initialization
153 else if (!calendar
) { // don't initialize twice
154 calendar
= new Calendar(element
, options
);
155 element
.data('fullCalendar', calendar
);
164 // function for adding/overriding defaults
165 function setDefaults(d
) {
166 mergeOptions(defaults
, d
);
170 // Recursively combines option hash-objects.
171 // Better than `$.extend(true, ...)` because arrays are not traversed/copied.
174 // mergeOptions(target, obj1, obj2, ...)
176 function mergeOptions(target
) {
178 function mergeIntoTarget(name
, value
) {
179 if ($.isPlainObject(value
) && $.isPlainObject(target
[name
]) && !isForcedAtomicOption(name
)) {
180 // merge into a new object to avoid destruction
181 target
[name
] = mergeOptions({}, target
[name
], value
); // combine. `value` object takes precedence
183 else if (value
!== undefined) { // only use values that are set and not undefined
184 target
[name
] = value
;
188 for (var i
=1; i
<arguments
.length
; i
++) {
189 $.each(arguments
[i
], mergeIntoTarget
);
196 // overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
197 function isForcedAtomicOption(name
) {
198 // Any option that ends in "Time" or "Duration" is probably a Duration,
199 // and these will commonly be specified as plain objects, which we don't want to mess up.
200 return /(Time|Duration)$/.test(name
);
202 // FIX: find a different solution for view-option-hashes and have a whitelist
203 // for options that can be recursively merged.
205 var langOptionHash
= fc
.langs
= {}; // initialize and expose
208 // TODO: document the structure and ordering of a FullCalendar lang file
209 // TODO: rename everything "lang" to "locale", like what the moment project did
212 // Initialize jQuery UI datepicker translations while using some of the translations
213 // Will set this as the default language for datepicker.
214 fc
.datepickerLang = function(langCode
, dpLangCode
, dpOptions
) {
216 // get the FullCalendar internal option hash for this language. create if necessary
217 var fcOptions
= langOptionHash
[langCode
] || (langOptionHash
[langCode
] = {});
219 // transfer some simple options from datepicker to fc
220 fcOptions
.isRTL
= dpOptions
.isRTL
;
221 fcOptions
.weekNumberTitle
= dpOptions
.weekHeader
;
223 // compute some more complex options from datepicker
224 $.each(dpComputableOptions
, function(name
, func
) {
225 fcOptions
[name
] = func(dpOptions
);
228 // is jQuery UI Datepicker is on the page?
231 // Register the language data.
232 // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
233 // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
234 // Make an alias so the language can be referenced either way.
235 $.datepicker
.regional
[dpLangCode
] =
236 $.datepicker
.regional
[langCode
] = // alias
239 // Alias 'en' to the default language data. Do this every time.
240 $.datepicker
.regional
.en
= $.datepicker
.regional
[''];
242 // Set as Datepicker's global defaults.
243 $.datepicker
.setDefaults(dpOptions
);
248 // Sets FullCalendar-specific translations. Will set the language as the global default.
249 fc
.lang = function(langCode
, newFcOptions
) {
253 // get the FullCalendar internal option hash for this language. create if necessary
254 fcOptions
= langOptionHash
[langCode
] || (langOptionHash
[langCode
] = {});
256 // provided new options for this language? merge them in
258 mergeOptions(fcOptions
, newFcOptions
);
261 // compute language options that weren't defined.
262 // always do this. newFcOptions can be undefined when initializing from i18n file,
263 // so no way to tell if this is an initialization or a default-setting.
264 momOptions
= getMomentLocaleData(langCode
); // will fall back to en
265 $.each(momComputableOptions
, function(name
, func
) {
266 if (fcOptions
[name
] === undefined) {
267 fcOptions
[name
] = func(momOptions
, fcOptions
);
271 // set it as the default language for FullCalendar
272 defaults
.lang
= langCode
;
276 // NOTE: can't guarantee any of these computations will run because not every language has datepicker
277 // configs, so make sure there are English fallbacks for these in the defaults file.
278 var dpComputableOptions
= {
280 defaultButtonText: function(dpOptions
) {
282 // the translations sometimes wrongly contain HTML entities
283 prev
: stripHtmlEntities(dpOptions
.prevText
),
284 next
: stripHtmlEntities(dpOptions
.nextText
),
285 today
: stripHtmlEntities(dpOptions
.currentText
)
289 // Produces format strings like "MMMM YYYY" -> "September 2014"
290 monthYearFormat: function(dpOptions
) {
291 return dpOptions
.showMonthAfterYear
?
292 'YYYY[' + dpOptions
.yearSuffix
+ '] MMMM' :
293 'MMMM YYYY[' + dpOptions
.yearSuffix
+ ']';
298 var momComputableOptions
= {
300 // Produces format strings like "ddd MM/DD" -> "Fri 12/10"
301 dayOfMonthFormat: function(momOptions
, fcOptions
) {
302 var format
= momOptions
.longDateFormat('l'); // for the format like "M/D/YYYY"
304 // strip the year off the edge, as well as other misc non-whitespace chars
305 format
= format
.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
307 if (fcOptions
.isRTL
) {
308 format
+= ' ddd'; // for RTL, add day-of-week to end
311 format
= 'ddd ' + format
; // for LTR, add day-of-week to beginning
316 // Produces format strings like "H(:mm)a" -> "6pm" or "6:30pm"
317 smallTimeFormat: function(momOptions
) {
318 return momOptions
.longDateFormat('LT')
319 .replace(':mm', '(:mm)')
320 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
321 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
324 // Produces format strings like "H(:mm)t" -> "6p" or "6:30p"
325 extraSmallTimeFormat: function(momOptions
) {
326 return momOptions
.longDateFormat('LT')
327 .replace(':mm', '(:mm)')
328 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
329 .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
332 // Produces format strings like "H:mm" -> "6:30" (with no AM/PM)
333 noMeridiemTimeFormat: function(momOptions
) {
334 return momOptions
.longDateFormat('LT')
335 .replace(/\s*a$/i, ''); // remove trailing AM/PM
341 // Returns moment's internal locale data. If doesn't exist, returns English.
342 // Works with moment-pre-2.8
343 function getMomentLocaleData(langCode
) {
344 var func
= moment
.localeData
|| moment
.langData
;
345 return func
.call(moment
, langCode
) ||
346 func
.call(moment
, 'en'); // the newer localData could return null, so fall back to en
350 // Initialize English by forcing computation of moment-derived options.
351 // Also, sets it as the default.
352 fc
.lang('en', englishDefaults
);
355 fc
.intersectionToSeg
= intersectionToSeg
;
356 fc
.applyAll
= applyAll
;
357 fc
.debounce
= debounce
;
360 /* FullCalendar-specific DOM Utilities
361 ----------------------------------------------------------------------------------------------------------------------*/
364 // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
365 // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
366 function compensateScroll(rowEls
, scrollbarWidths
) {
367 if (scrollbarWidths
.left
) {
369 'border-left-width': 1,
370 'margin-left': scrollbarWidths
.left
- 1
373 if (scrollbarWidths
.right
) {
375 'border-right-width': 1,
376 'margin-right': scrollbarWidths
.right
- 1
382 // Undoes compensateScroll and restores all borders/margins
383 function uncompensateScroll(rowEls
) {
387 'border-left-width': '',
388 'border-right-width': ''
393 // Make the mouse cursor express that an event is not allowed in the current area
394 function disableCursor() {
395 $('body').addClass('fc-not-allowed');
399 // Returns the mouse cursor to its original look
400 function enableCursor() {
401 $('body').removeClass('fc-not-allowed');
405 // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
406 // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
407 // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
408 // reduces the available height.
409 function distributeHeight(els
, availableHeight
, shouldRedistribute
) {
411 // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
412 // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
414 var minOffset1
= Math
.floor(availableHeight
/ els
.length
); // for non-last element
415 var minOffset2
= Math
.floor(availableHeight
- minOffset1
* (els
.length
- 1)); // for last element *FLOORING NOTE*
416 var flexEls
= []; // elements that are allowed to expand. array of DOM nodes
417 var flexOffsets
= []; // amount of vertical space it takes up
418 var flexHeights
= []; // actual css height
421 undistributeHeight(els
); // give all elements their natural height
423 // find elements that are below the recommended height (expandable).
424 // important to query for heights in a single first pass (to avoid reflow oscillation).
425 els
.each(function(i
, el
) {
426 var minOffset
= i
=== els
.length
- 1 ? minOffset2
: minOffset1
;
427 var naturalOffset
= $(el
).outerHeight(true);
429 if (naturalOffset
< minOffset
) {
431 flexOffsets
.push(naturalOffset
);
432 flexHeights
.push($(el
).height());
435 // this element stretches past recommended height (non-expandable). mark the space as occupied.
436 usedHeight
+= naturalOffset
;
440 // readjust the recommended height to only consider the height available to non-maxed-out rows.
441 if (shouldRedistribute
) {
442 availableHeight
-= usedHeight
;
443 minOffset1
= Math
.floor(availableHeight
/ flexEls
.length
);
444 minOffset2
= Math
.floor(availableHeight
- minOffset1
* (flexEls
.length
- 1)); // *FLOORING NOTE*
447 // assign heights to all expandable elements
448 $(flexEls
).each(function(i
, el
) {
449 var minOffset
= i
=== flexEls
.length
- 1 ? minOffset2
: minOffset1
;
450 var naturalOffset
= flexOffsets
[i
];
451 var naturalHeight
= flexHeights
[i
];
452 var newHeight
= minOffset
- (naturalOffset
- naturalHeight
); // subtract the margin/padding
454 if (naturalOffset
< minOffset
) { // we check this again because redistribution might have changed things
455 $(el
).height(newHeight
);
461 // Undoes distrubuteHeight, restoring all els to their natural height
462 function undistributeHeight(els
) {
467 // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
468 // cells to be that width.
469 // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
470 function matchCellWidths(els
) {
471 var maxInnerWidth
= 0;
473 els
.find('> *').each(function(i
, innerEl
) {
474 var innerWidth
= $(innerEl
).outerWidth();
475 if (innerWidth
> maxInnerWidth
) {
476 maxInnerWidth
= innerWidth
;
480 maxInnerWidth
++; // sometimes not accurate of width the text needs to stay on one line. insurance
482 els
.width(maxInnerWidth
);
484 return maxInnerWidth
;
488 // Turns a container element into a scroller if its contents is taller than the allotted height.
489 // Returns true if the element is now a scroller, false otherwise.
490 // NOTE: this method is best because it takes weird zooming dimensions into account
491 function setPotentialScroller(containerEl
, height
) {
492 containerEl
.height(height
).addClass('fc-scroller');
494 // are scrollbars needed?
495 if (containerEl
[0].scrollHeight
- 1 > containerEl
[0].clientHeight
) { // !!! -1 because IE is often off-by-one :(
499 unsetScroller(containerEl
); // undo
504 // Takes an element that might have been a scroller, and turns it back into a normal element.
505 function unsetScroller(containerEl
) {
506 containerEl
.height('').removeClass('fc-scroller');
510 /* General DOM Utilities
511 ----------------------------------------------------------------------------------------------------------------------*/
514 // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
515 function getScrollParent(el
) {
516 var position
= el
.css('position'),
517 scrollParent
= el
.parents().filter(function() {
518 var parent
= $(this);
519 return (/(auto|scroll)/).test(
520 parent
.css('overflow') + parent
.css('overflow-y') + parent
.css('overflow-x')
524 return position
=== 'fixed' || !scrollParent
.length
? $(el
[0].ownerDocument
|| document
) : scrollParent
;
528 // Given a container element, return an object with the pixel values of the left/right scrollbars.
529 // Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
530 // PREREQUISITE: container element must have a single child with display:block
531 function getScrollbarWidths(container
) {
532 var containerLeft
= container
.offset().left
;
533 var containerRight
= containerLeft
+ container
.width();
534 var inner
= container
.children();
535 var innerLeft
= inner
.offset().left
;
536 var innerRight
= innerLeft
+ inner
.outerWidth();
539 left
: innerLeft
- containerLeft
,
540 right
: containerRight
- innerRight
545 // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
546 function isPrimaryMouseButton(ev
) {
547 return ev
.which
== 1 && !ev
.ctrlKey
;
551 /* FullCalendar-specific Misc Utilities
552 ----------------------------------------------------------------------------------------------------------------------*/
555 // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
556 // Expects all dates to be normalized to the same timezone beforehand.
557 // TODO: move to date section?
558 function intersectionToSeg(subjectRange
, constraintRange
) {
559 var subjectStart
= subjectRange
.start
;
560 var subjectEnd
= subjectRange
.end
;
561 var constraintStart
= constraintRange
.start
;
562 var constraintEnd
= constraintRange
.end
;
563 var segStart
, segEnd
;
566 if (subjectEnd
> constraintStart
&& subjectStart
< constraintEnd
) { // in bounds at all?
568 if (subjectStart
>= constraintStart
) {
569 segStart
= subjectStart
.clone();
573 segStart
= constraintStart
.clone();
577 if (subjectEnd
<= constraintEnd
) {
578 segEnd
= subjectEnd
.clone();
582 segEnd
= constraintEnd
.clone();
596 function smartProperty(obj
, name
) { // get a camel-cased/namespaced property of an object
598 if (obj
[name
] !== undefined) {
601 var parts
= name
.split(/(?=[A-Z])/),
602 i
= parts
.length
- 1, res
;
604 res
= obj
[parts
[i
].toLowerCase()];
605 if (res
!== undefined) {
609 return obj
['default'];
614 ----------------------------------------------------------------------------------------------------------------------*/
616 var dayIDs
= [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
617 var intervalUnits
= [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
620 // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
621 // Moments will have their timezones normalized.
622 function diffDayTime(a
, b
) {
623 return moment
.duration({
624 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days'),
625 ms
: a
.time() - b
.time() // time-of-day from day start. disregards timezone
630 // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
631 function diffDay(a
, b
) {
632 return moment
.duration({
633 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days')
638 // Computes the larges whole-unit period of time, as a duration object.
639 // For example, 48 hours will be {days:2} whereas 49 hours will be {hours:49}.
640 // Accepts start/end, a range object, or an original duration object.
642 function computeIntervalDuration(start, end) {
643 var durationInput = {};
647 for (i = 0; i < intervalUnits.length; i++) {
648 unit = intervalUnits[i];
649 val = computeIntervalAs(unit, start, end);
655 durationInput[unit] = val;
656 return moment.duration(durationInput);
661 // Computes the unit name of the largest whole-unit period of time.
662 // For example, 48 hours will be "days" wherewas 49 hours will be "hours".
663 // Accepts start/end, a range object, or an original duration object.
664 function computeIntervalUnit(start
, end
) {
667 for (i
= 0; i
< intervalUnits
.length
; i
++) {
668 unit
= intervalUnits
[i
];
669 if (computeIntervalAs(unit
, start
, end
)) {
674 return unit
; // will be "milliseconds" if nothing else matches
678 // Computes the number of units the interval is cleanly comprised of.
679 // If the given unit does not cleanly divide the interval a whole number of times, `false` is returned.
680 // Accepts start/end, a range object, or an original duration object.
681 function computeIntervalAs(unit
, start
, end
) {
684 if (end
!= null) { // given start, end
685 val
= end
.diff(start
, unit
, true);
687 else if (moment
.isDuration(start
)) { // given duration
688 val
= start
.as(unit
);
690 else { // given { start, end } range object
691 val
= start
.end
.diff(start
.start
, unit
, true);
694 if (val
>= 1 && isInt(val
)) {
702 function isNativeDate(input
) {
703 return Object
.prototype.toString
.call(input
) === '[object Date]' || input
instanceof Date
;
707 // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
708 function isTimeString(str
) {
709 return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str
);
714 ----------------------------------------------------------------------------------------------------------------------*/
716 var hasOwnPropMethod
= {}.hasOwnProperty
;
719 // Create an object that has the given prototype. Just like Object.create
720 function createObject(proto
) {
721 var f = function() {};
727 function copyOwnProps(src
, dest
) {
728 for (var name
in src
) {
729 if (hasOwnProp(src
, name
)) {
730 dest
[name
] = src
[name
];
736 function hasOwnProp(obj
, name
) {
737 return hasOwnPropMethod
.call(obj
, name
);
741 // Is the given value a non-object non-function value?
742 function isAtomic(val
) {
743 return /undefined|null|boolean|number|string/.test($.type(val
));
747 function applyAll(functions
, thisObj
, args
) {
748 if ($.isFunction(functions
)) {
749 functions
= [ functions
];
754 for (i
=0; i
<functions
.length
; i
++) {
755 ret
= functions
[i
].apply(thisObj
, args
) || ret
;
762 function firstDefined() {
763 for (var i
=0; i
<arguments
.length
; i
++) {
764 if (arguments
[i
] !== undefined) {
771 function htmlEscape(s
) {
772 return (s
+ '').replace(/&/g
, '&')
773 .replace(/</g
, '<')
774 .replace(/>/g
, '>')
775 .replace(/'/g, ''')
776 .replace(/"/g, '"
;')
777 .replace(/\n/g, '<br
/>');
781 function stripHtmlEntities(text) {
782 return text.replace(/&.*?;/g, '');
786 function capitaliseFirstLetter(str) {
787 return str.charAt(0).toUpperCase() + str.slice(1);
791 function compareNumbers(a, b) { // for .sort()
801 // Returns a function, that, as long as it continues to be invoked, will not
802 // be triggered. The function will be called after it stops being called for
804 // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
805 function debounce(func, wait) {
809 var timestamp; // of most recent call
810 var later = function() {
811 var last = +new Date() - timestamp;
812 if (last < wait && last > 0) {
813 timeoutId = setTimeout(later, wait - last);
817 func.apply(context, args);
819 context = args = null;
827 timestamp = +new Date();
829 timeoutId = setTimeout(later, wait);
834 var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
835 var ambigTimeOrZoneRegex =
836 /^\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+)?)?)?)?)?$/;
837 var newMomentProto = moment.fn; // where we will attach our new methods
838 var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
839 var allowValueOptimization;
840 var setUTCValues; // function defined below
841 var setLocalValues; // function defined below
845 // -------------------------------------------------------------------------------------------------
847 // Creates a new moment, similar to the vanilla moment(...) constructor, but with
848 // extra features (ambiguous time, enhanced formatting). When given an existing moment,
849 // it will function as a clone (and retain the zone of the moment). Anything else will
850 // result in a moment in the local zone.
851 fc.moment = function() {
852 return makeMoment(arguments);
855 // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
856 fc.moment.utc = function() {
857 var mom = makeMoment(arguments, true);
859 // Force it into UTC because makeMoment doesn't guarantee it
860 // (if given a pre-existing moment for example)
861 if (mom
.hasTime()) { // don't give ambiguously-timed moments a UTC zone
868 // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
869 // ISO8601 strings with no timezone offset will become ambiguously zoned.
870 fc
.moment
.parseZone = function() {
871 return makeMoment(arguments
, true, true);
874 // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
875 // native Date, or called with no arguments (the current time), the resulting moment will be local.
876 // Anything else needs to be "parsed" (a string or an array), and will be affected by:
877 // parseAsUTC - if there is no zone information, should we parse the input in UTC?
878 // parseZone - if there is zone information, should we force the zone of the moment?
879 function makeMoment(args
, parseAsUTC
, parseZone
) {
881 var isSingleString
= args
.length
== 1 && typeof input
=== 'string';
887 if (moment
.isMoment(input
)) {
888 mom
= moment
.apply(null, args
); // clone it
889 transferAmbigs(input
, mom
); // the ambig flags weren't transfered with the clone
891 else if (isNativeDate(input
) || input
=== undefined) {
892 mom
= moment
.apply(null, args
); // will be local
894 else { // "parsing" is required
898 if (isSingleString
) {
899 if (ambigDateOfMonthRegex
.test(input
)) {
900 // accept strings like '2014-05', but convert to the first of the month
902 args
= [ input
]; // for when we pass it on to moment's constructor
906 else if ((ambigMatch
= ambigTimeOrZoneRegex
.exec(input
))) {
907 isAmbigTime
= !ambigMatch
[5]; // no time part?
911 else if ($.isArray(input
)) {
912 // arrays have no timezone information, so assume ambiguous zone
915 // otherwise, probably a string with a format
917 if (parseAsUTC
|| isAmbigTime
) {
918 mom
= moment
.utc
.apply(moment
, args
);
921 mom
= moment
.apply(null, args
);
925 mom
._ambigTime
= true;
926 mom
._ambigZone
= true; // ambiguous time always means ambiguous zone
928 else if (parseZone
) { // let's record the inputted zone somehow
930 mom
._ambigZone
= true;
932 else if (isSingleString
) {
933 mom
.zone(input
); // if not a valid zone, will assign UTC
938 mom
._fullCalendar
= true; // flag for extended functionality
944 // A clone method that works with the flags related to our enhanced functionality.
945 // In the future, use moment.momentProperties
946 newMomentProto
.clone = function() {
947 var mom
= oldMomentProto
.clone
.apply(this, arguments
);
949 // these flags weren't transfered with the clone
950 transferAmbigs(this, mom
);
951 if (this._fullCalendar
) {
952 mom
._fullCalendar
= true;
960 // -------------------------------------------------------------------------------------------------
963 // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
964 // If the moment has an ambiguous time, a duration of 00:00 will be returned.
967 // You can supply a Duration, a Moment, or a Duration-like argument.
968 // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
969 newMomentProto
.time = function(time
) {
971 // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
972 // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
973 if (!this._fullCalendar
) {
974 return oldMomentProto
.time
.apply(this, arguments
);
977 if (time
== null) { // getter
978 return moment
.duration({
980 minutes
: this.minutes(),
981 seconds
: this.seconds(),
982 milliseconds
: this.milliseconds()
987 this._ambigTime
= false; // mark that the moment now has a time
989 if (!moment
.isDuration(time
) && !moment
.isMoment(time
)) {
990 time
= moment
.duration(time
);
993 // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
994 // Only for Duration times, not Moment times.
996 if (moment
.isDuration(time
)) {
997 dayHours
= Math
.floor(time
.asDays()) * 24;
1000 // We need to set the individual fields.
1001 // Can't use startOf('day') then add duration. In case of DST at start of day.
1002 return this.hours(dayHours
+ time
.hours())
1003 .minutes(time
.minutes())
1004 .seconds(time
.seconds())
1005 .milliseconds(time
.milliseconds());
1009 // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
1010 // but preserving its YMD. A moment with a stripped time will display no time
1011 // nor timezone offset when .format() is called.
1012 newMomentProto
.stripTime = function() {
1015 if (!this._ambigTime
) {
1017 // get the values before any conversion happens
1018 a
= this.toArray(); // array of y/m/d/h/m/s/ms
1020 this.utc(); // set the internal UTC flag (will clear the ambig flags)
1021 setUTCValues(this, a
.slice(0, 3)); // set the year/month/date. time will be zero
1023 // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
1024 // which clears all ambig flags. Same with setUTCValues with moment-timezone.
1025 this._ambigTime
= true;
1026 this._ambigZone
= true; // if ambiguous time, also ambiguous timezone offset
1029 return this; // for chaining
1032 // Returns if the moment has a non-ambiguous time (boolean)
1033 newMomentProto
.hasTime = function() {
1034 return !this._ambigTime
;
1039 // -------------------------------------------------------------------------------------------------
1041 // Converts the moment to UTC, stripping out its timezone offset, but preserving its
1042 // YMD and time-of-day. A moment with a stripped timezone offset will display no
1043 // timezone offset when .format() is called.
1044 newMomentProto
.stripZone = function() {
1045 var a
, wasAmbigTime
;
1047 if (!this._ambigZone
) {
1049 // get the values before any conversion happens
1050 a
= this.toArray(); // array of y/m/d/h/m/s/ms
1051 wasAmbigTime
= this._ambigTime
;
1053 this.utc(); // set the internal UTC flag (will clear the ambig flags)
1054 setUTCValues(this, a
); // will set the year/month/date/hours/minutes/seconds/ms
1057 // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
1058 this._ambigTime
= true;
1061 // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
1062 // which clears all ambig flags. Same with setUTCValues with moment-timezone.
1063 this._ambigZone
= true;
1066 return this; // for chaining
1069 // Returns of the moment has a non-ambiguous timezone offset (boolean)
1070 newMomentProto
.hasZone = function() {
1071 return !this._ambigZone
;
1074 // this method implicitly marks a zone (will get called upon .utc() and .local())
1075 newMomentProto
.zone = function(tzo
) {
1077 if (tzo
!= null) { // setter
1078 // these assignments needs to happen before the original zone method is called.
1079 // I forget why, something to do with a browser crash.
1080 this._ambigTime
= false;
1081 this._ambigZone
= false;
1084 return oldMomentProto
.zone
.apply(this, arguments
);
1087 // this method implicitly marks a zone
1088 newMomentProto
.local = function() {
1089 var a
= this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
1090 var wasAmbigZone
= this._ambigZone
;
1092 oldMomentProto
.local
.apply(this, arguments
); // will clear ambig flags
1095 // If the moment was ambiguously zoned, the date fields were stored as UTC.
1096 // We want to preserve these, but in local time.
1097 setLocalValues(this, a
);
1100 return this; // for chaining
1105 // -------------------------------------------------------------------------------------------------
1107 newMomentProto
.format = function() {
1108 if (this._fullCalendar
&& arguments
[0]) { // an enhanced moment? and a format string provided?
1109 return formatDate(this, arguments
[0]); // our extended formatting
1111 if (this._ambigTime
) {
1112 return oldMomentFormat(this, 'YYYY-MM-DD');
1114 if (this._ambigZone
) {
1115 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1117 return oldMomentProto
.format
.apply(this, arguments
);
1120 newMomentProto
.toISOString = function() {
1121 if (this._ambigTime
) {
1122 return oldMomentFormat(this, 'YYYY-MM-DD');
1124 if (this._ambigZone
) {
1125 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1127 return oldMomentProto
.toISOString
.apply(this, arguments
);
1132 // -------------------------------------------------------------------------------------------------
1134 // Is the moment within the specified range? `end` is exclusive.
1135 // FYI, this method is not a standard Moment method, so always do our enhanced logic.
1136 newMomentProto
.isWithin = function(start
, end
) {
1137 var a
= commonlyAmbiguate([ this, start
, end
]);
1138 return a
[0] >= a
[1] && a
[0] < a
[2];
1141 // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
1142 // If no units specified, the two moments must be identically the same, with matching ambig flags.
1143 newMomentProto
.isSame = function(input
, units
) {
1146 // only do custom logic if this is an enhanced moment
1147 if (!this._fullCalendar
) {
1148 return oldMomentProto
.isSame
.apply(this, arguments
);
1152 a
= commonlyAmbiguate([ this, input
], true); // normalize timezones but don't erase times
1153 return oldMomentProto
.isSame
.call(a
[0], a
[1], units
);
1156 input
= fc
.moment
.parseZone(input
); // normalize input
1157 return oldMomentProto
.isSame
.call(this, input
) &&
1158 Boolean(this._ambigTime
) === Boolean(input
._ambigTime
) &&
1159 Boolean(this._ambigZone
) === Boolean(input
._ambigZone
);
1163 // Make these query methods work with ambiguous moments
1167 ], function(i
, methodName
) {
1168 newMomentProto
[methodName
] = function(input
, units
) {
1171 // only do custom logic if this is an enhanced moment
1172 if (!this._fullCalendar
) {
1173 return oldMomentProto
[methodName
].apply(this, arguments
);
1176 a
= commonlyAmbiguate([ this, input
]);
1177 return oldMomentProto
[methodName
].call(a
[0], a
[1], units
);
1183 // -------------------------------------------------------------------------------------------------
1185 // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
1186 // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
1187 // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
1188 // returns the original moments if no modifications are necessary.
1189 function commonlyAmbiguate(inputs
, preserveTime
) {
1190 var anyAmbigTime
= false;
1191 var anyAmbigZone
= false;
1192 var len
= inputs
.length
;
1196 // parse inputs into real moments and query their ambig flags
1197 for (i
= 0; i
< len
; i
++) {
1199 if (!moment
.isMoment(mom
)) {
1200 mom
= fc
.moment
.parseZone(mom
);
1202 anyAmbigTime
= anyAmbigTime
|| mom
._ambigTime
;
1203 anyAmbigZone
= anyAmbigZone
|| mom
._ambigZone
;
1207 // strip each moment down to lowest common ambiguity
1208 // use clones to avoid modifying the original moments
1209 for (i
= 0; i
< len
; i
++) {
1211 if (!preserveTime
&& anyAmbigTime
&& !mom
._ambigTime
) {
1212 moms
[i
] = mom
.clone().stripTime();
1214 else if (anyAmbigZone
&& !mom
._ambigZone
) {
1215 moms
[i
] = mom
.clone().stripZone();
1222 // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
1223 function transferAmbigs(src
, dest
) {
1224 if (src
._ambigTime
) {
1225 dest
._ambigTime
= true;
1227 else if (dest
._ambigTime
) {
1228 dest
._ambigTime
= false;
1231 if (src
._ambigZone
) {
1232 dest
._ambigZone
= true;
1234 else if (dest
._ambigZone
) {
1235 dest
._ambigZone
= false;
1240 // Sets the year/month/date/etc values of the moment from the given array.
1241 // Inefficient because it calls each individual setter.
1242 function setMomentValues(mom
, a
) {
1249 .milliseconds(a
[6] || 0);
1252 // Can we set the moment's internal date directly?
1253 allowValueOptimization
= '_d' in moment() && 'updateOffset' in moment
;
1255 // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
1256 // Assumes the given moment is already in UTC mode.
1257 setUTCValues
= allowValueOptimization
? function(mom
, a
) {
1258 // simlate what moment's accessors do
1259 mom
._d
.setTime(Date
.UTC
.apply(Date
, a
));
1260 moment
.updateOffset(mom
, false); // keepTime=false
1261 } : setMomentValues
;
1263 // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
1264 // Assumes the given moment is already in local mode.
1265 setLocalValues
= allowValueOptimization
? function(mom
, a
) {
1266 // simlate what moment's accessors do
1267 mom
._d
.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
1276 moment
.updateOffset(mom
, false); // keepTime=false
1277 } : setMomentValues
;
1279 // Single Date Formatting
1280 // -------------------------------------------------------------------------------------------------
1283 // call this if you want Moment's original format method to be used
1284 function oldMomentFormat(mom
, formatStr
) {
1285 return oldMomentProto
.format
.call(mom
, formatStr
); // oldMomentProto defined in moment-ext.js
1289 // Formats `date` with a Moment formatting string, but allow our non-zero areas and
1290 // additional token.
1291 function formatDate(date
, formatStr
) {
1292 return formatDateWithChunks(date
, getFormatStringChunks(formatStr
));
1296 function formatDateWithChunks(date
, chunks
) {
1300 for (i
=0; i
<chunks
.length
; i
++) {
1301 s
+= formatDateWithChunk(date
, chunks
[i
]);
1308 // addition formatting tokens we want recognized
1309 var tokenOverrides
= {
1310 t: function(date
) { // "a" or "p"
1311 return oldMomentFormat(date
, 'a').charAt(0);
1313 T: function(date
) { // "A" or "P"
1314 return oldMomentFormat(date
, 'A').charAt(0);
1319 function formatDateWithChunk(date
, chunk
) {
1323 if (typeof chunk
=== 'string') { // a literal string
1326 else if ((token
= chunk
.token
)) { // a token, like "YYYY"
1327 if (tokenOverrides
[token
]) {
1328 return tokenOverrides
[token
](date
); // use our custom token
1330 return oldMomentFormat(date
, token
);
1332 else if (chunk
.maybe
) { // a grouping of other chunks that must be non-zero
1333 maybeStr
= formatDateWithChunks(date
, chunk
.maybe
);
1334 if (maybeStr
.match(/[1-9]/)) {
1343 // Date Range Formatting
1344 // -------------------------------------------------------------------------------------------------
1345 // TODO: make it work with timezone offset
1347 // Using a formatting string meant for a single date, generate a range string, like
1348 // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1349 // If the dates are the same as far as the format string is concerned, just return a single
1350 // rendering of one date, without any separator.
1351 function formatRange(date1
, date2
, formatStr
, separator
, isRTL
) {
1354 date1
= fc
.moment
.parseZone(date1
);
1355 date2
= fc
.moment
.parseZone(date2
);
1357 localeData
= (date1
.localeData
|| date1
.lang
).call(date1
); // works with moment-pre-2.8
1359 // Expand localized format strings, like "LL" -> "MMMM D YYYY"
1360 formatStr
= localeData
.longDateFormat(formatStr
) || formatStr
;
1361 // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1362 // or non-zero areas in Moment's localized format strings.
1364 separator
= separator
|| ' - ';
1366 return formatRangeWithChunks(
1369 getFormatStringChunks(formatStr
),
1374 fc
.formatRange
= formatRange
; // expose
1377 function formatRangeWithChunks(date1
, date2
, chunks
, separator
, isRTL
) {
1378 var chunkStr
; // the rendering of the chunk
1384 var middleStr1
= '';
1385 var middleStr2
= '';
1388 // Start at the leftmost side of the formatting string and continue until you hit a token
1389 // that is not the same between dates.
1390 for (leftI
=0; leftI
<chunks
.length
; leftI
++) {
1391 chunkStr
= formatSimilarChunk(date1
, date2
, chunks
[leftI
]);
1392 if (chunkStr
=== false) {
1395 leftStr
+= chunkStr
;
1398 // Similarly, start at the rightmost side of the formatting string and move left
1399 for (rightI
=chunks
.length
-1; rightI
>leftI
; rightI
--) {
1400 chunkStr
= formatSimilarChunk(date1
, date2
, chunks
[rightI
]);
1401 if (chunkStr
=== false) {
1404 rightStr
= chunkStr
+ rightStr
;
1407 // The area in the middle is different for both of the dates.
1408 // Collect them distinctly so we can jam them together later.
1409 for (middleI
=leftI
; middleI
<=rightI
; middleI
++) {
1410 middleStr1
+= formatDateWithChunk(date1
, chunks
[middleI
]);
1411 middleStr2
+= formatDateWithChunk(date2
, chunks
[middleI
]);
1414 if (middleStr1
|| middleStr2
) {
1416 middleStr
= middleStr2
+ separator
+ middleStr1
;
1419 middleStr
= middleStr1
+ separator
+ middleStr2
;
1423 return leftStr
+ middleStr
+ rightStr
;
1427 var similarUnitMap
= {
1430 D
: 'day', // day of month
1431 d
: 'day', // day of week
1432 // prevents a separator between anything time-related...
1433 A
: 'second', // AM/PM
1434 a
: 'second', // am/pm
1437 H
: 'second', // hour (24)
1438 h
: 'second', // hour (12)
1439 m
: 'second', // minute
1440 s
: 'second' // second
1442 // TODO: week maybe?
1445 // Given a formatting chunk, and given that both dates are similar in the regard the
1446 // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
1447 function formatSimilarChunk(date1
, date2
, chunk
) {
1451 if (typeof chunk
=== 'string') { // a literal string
1454 else if ((token
= chunk
.token
)) {
1455 unit
= similarUnitMap
[token
.charAt(0)];
1456 // are the dates the same for this unit of measurement?
1457 if (unit
&& date1
.isSame(date2
, unit
)) {
1458 return oldMomentFormat(date1
, token
); // would be the same if we used `date2`
1459 // BTW, don't support custom tokens
1463 return false; // the chunk is NOT the same for the two dates
1464 // BTW, don't support splitting on non-zero areas
1469 // -------------------------------------------------------------------------------------------------
1472 var formatStringChunkCache
= {};
1475 function getFormatStringChunks(formatStr
) {
1476 if (formatStr
in formatStringChunkCache
) {
1477 return formatStringChunkCache
[formatStr
];
1479 return (formatStringChunkCache
[formatStr
] = chunkFormatString(formatStr
));
1483 // Break the formatting string into an array of chunks
1484 function chunkFormatString(formatStr
) {
1486 var chunker
= /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
1489 while ((match
= chunker
.exec(formatStr
))) {
1490 if (match
[1]) { // a literal string inside [ ... ]
1491 chunks
.push(match
[1]);
1493 else if (match
[2]) { // non-zero formatting inside ( ... )
1494 chunks
.push({ maybe
: chunkFormatString(match
[2]) });
1496 else if (match
[3]) { // a formatting token
1497 chunks
.push({ token
: match
[3] });
1499 else if (match
[5]) { // an unenclosed literal string
1500 chunks
.push(match
[5]);
1507 fc
.Class
= Class
; // export
1509 // class that all other classes will inherit from
1510 function Class() { }
1512 // called upon a class to create a subclass
1513 Class
.extend = function(members
) {
1514 var superClass
= this;
1517 members
= members
|| {};
1519 // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
1520 if (hasOwnProp(members
, 'constructor')) {
1521 subClass
= members
.constructor;
1523 if (typeof subClass
!== 'function') {
1524 subClass
= members
.constructor = function() {
1525 superClass
.apply(this, arguments
);
1529 // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
1530 subClass
.prototype = createObject(superClass
.prototype);
1532 // copy each member variable/method onto the the subclass's prototype
1533 copyOwnProps(members
, subClass
.prototype);
1535 // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1536 copyOwnProps(superClass
, subClass
);
1541 // adds new member variables/methods to the class's prototype.
1542 // can be called with another class, or a plain object hash containing new members.
1543 Class
.mixin = function(members
) {
1544 copyOwnProps(members
.prototype || members
, this.prototype);
1546 /* A rectangular panel that is absolutely positioned over other content
1547 ------------------------------------------------------------------------------------------------------------------------
1549 - className (string)
1550 - content (HTML string or jQuery element set)
1554 - right (the x coord of where the right edge should be. not a "CSS" right)
1555 - autoHide (boolean)
1560 var Popover
= Class
.extend({
1564 el
: null, // the container element for the popover. generated by this object
1565 documentMousedownProxy
: null, // document mousedown handler bound to `this`
1566 margin
: 10, // the space required between the popover and the edges of the scroll container
1569 constructor: function(options
) {
1570 this.options
= options
|| {};
1574 // Shows the popover on the specified position. Renders it if not already
1576 if (this.isHidden
) {
1582 this.isHidden
= false;
1583 this.trigger('show');
1588 // Hides the popover, through CSS, but does not remove it from the DOM
1590 if (!this.isHidden
) {
1592 this.isHidden
= true;
1593 this.trigger('hide');
1598 // Creates `this.el` and renders content inside of it
1599 render: function() {
1601 var options
= this.options
;
1603 this.el
= $('<div class="fc-popover"/>')
1604 .addClass(options
.className
|| '')
1606 // position initially to the top left to avoid creating scrollbars
1610 .append(options
.content
)
1611 .appendTo(options
.parentEl
);
1613 // when a click happens on anything inside with a 'fc-close' className, hide the popover
1614 this.el
.on('click', '.fc-close', function() {
1618 if (options
.autoHide
) {
1619 $(document
).on('mousedown', this.documentMousedownProxy
= $.proxy(this, 'documentMousedown'));
1624 // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
1625 documentMousedown: function(ev
) {
1626 // only hide the popover if the click happened outside the popover
1627 if (this.el
&& !$(ev
.target
).closest(this.el
).length
) {
1633 // Hides and unregisters any handlers
1634 destroy: function() {
1642 $(document
).off('mousedown', this.documentMousedownProxy
);
1646 // Positions the popover optimally, using the top/left/right options
1647 position: function() {
1648 var options
= this.options
;
1649 var origin
= this.el
.offsetParent().offset();
1650 var width
= this.el
.outerWidth();
1651 var height
= this.el
.outerHeight();
1652 var windowEl
= $(window
);
1653 var viewportEl
= getScrollParent(this.el
);
1657 var top
; // the "position" (not "offset") values for the popover
1660 // compute top and left
1661 top
= options
.top
|| 0;
1662 if (options
.left
!== undefined) {
1663 left
= options
.left
;
1665 else if (options
.right
!== undefined) {
1666 left
= options
.right
- width
; // derive the left value from the right value
1672 if (viewportEl
.is(window
) || viewportEl
.is(document
)) { // normalize getScrollParent's result
1673 viewportEl
= windowEl
;
1674 viewportTop
= 0; // the window is always at the top left
1675 viewportLeft
= 0; // (and .offset() won't work if called here)
1678 viewportOffset
= viewportEl
.offset();
1679 viewportTop
= viewportOffset
.top
;
1680 viewportLeft
= viewportOffset
.left
;
1683 // if the window is scrolled, it causes the visible area to be further down
1684 viewportTop
+= windowEl
.scrollTop();
1685 viewportLeft
+= windowEl
.scrollLeft();
1687 // constrain to the view port. if constrained by two edges, give precedence to top/left
1688 if (options
.viewportConstrain
!== false) {
1689 top
= Math
.min(top
, viewportTop
+ viewportEl
.outerHeight() - height
- this.margin
);
1690 top
= Math
.max(top
, viewportTop
+ this.margin
);
1691 left
= Math
.min(left
, viewportLeft
+ viewportEl
.outerWidth() - width
- this.margin
);
1692 left
= Math
.max(left
, viewportLeft
+ this.margin
);
1696 top
: top
- origin
.top
,
1697 left
: left
- origin
.left
1702 // Triggers a callback. Calls a function in the option hash of the same name.
1703 // Arguments beyond the first `name` are forwarded on.
1704 // TODO: better code reuse for this. Repeat code
1705 trigger: function(name
) {
1706 if (this.options
[name
]) {
1707 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
1713 /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
1714 ------------------------------------------------------------------------------------------------------------------------
1717 CoordMap.prototype = {
1718 build: function() {},
1719 getCell: function(x, y) {}
1724 /* Coordinate map for a grid component
1725 ----------------------------------------------------------------------------------------------------------------------*/
1727 var GridCoordMap
= Class
.extend({
1729 grid
: null, // reference to the Grid
1730 rowCoords
: null, // array of {top,bottom} objects
1731 colCoords
: null, // array of {left,right} objects
1733 containerEl
: null, // container element that all coordinates are constrained to. optionally assigned
1735 maxX
: null, // exclusive
1737 maxY
: null, // exclusive
1740 constructor: function(grid
) {
1745 // Queries the grid for the coordinates of all the cells
1747 this.rowCoords
= this.grid
.computeRowCoords();
1748 this.colCoords
= this.grid
.computeColCoords();
1749 this.computeBounds();
1753 // Clears the coordinates data to free up memory
1755 this.rowCoords
= null;
1756 this.colCoords
= null;
1760 // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
1761 getCell: function(x
, y
) {
1762 var rowCoords
= this.rowCoords
;
1763 var colCoords
= this.colCoords
;
1769 if (this.inBounds(x
, y
)) {
1771 for (i
= 0; i
< rowCoords
.length
; i
++) {
1772 coords
= rowCoords
[i
];
1773 if (y
>= coords
.top
&& y
< coords
.bottom
) {
1779 for (i
= 0; i
< colCoords
.length
; i
++) {
1780 coords
= colCoords
[i
];
1781 if (x
>= coords
.left
&& x
< coords
.right
) {
1787 if (hitRow
!== null && hitCol
!== null) {
1788 cell
= this.grid
.getCell(hitRow
, hitCol
);
1789 cell
.grid
= this.grid
; // for DragListener's isCellsEqual. dragging between grids
1798 // If there is a containerEl, compute the bounds into min/max values
1799 computeBounds: function() {
1800 var containerOffset
;
1802 if (this.containerEl
) {
1803 containerOffset
= this.containerEl
.offset();
1804 this.minX
= containerOffset
.left
;
1805 this.maxX
= containerOffset
.left
+ this.containerEl
.outerWidth();
1806 this.minY
= containerOffset
.top
;
1807 this.maxY
= containerOffset
.top
+ this.containerEl
.outerHeight();
1812 // Determines if the given coordinates are in bounds. If no `containerEl`, always true
1813 inBounds: function(x
, y
) {
1814 if (this.containerEl
) {
1815 return x
>= this.minX
&& x
< this.maxX
&& y
>= this.minY
&& y
< this.maxY
;
1823 /* Coordinate map that is a combination of multiple other coordinate maps
1824 ----------------------------------------------------------------------------------------------------------------------*/
1826 var ComboCoordMap
= Class
.extend({
1828 coordMaps
: null, // an array of CoordMaps
1831 constructor: function(coordMaps
) {
1832 this.coordMaps
= coordMaps
;
1836 // Builds all coordMaps
1838 var coordMaps
= this.coordMaps
;
1841 for (i
= 0; i
< coordMaps
.length
; i
++) {
1842 coordMaps
[i
].build();
1847 // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
1848 getCell: function(x
, y
) {
1849 var coordMaps
= this.coordMaps
;
1853 for (i
= 0; i
< coordMaps
.length
&& !cell
; i
++) {
1854 cell
= coordMaps
[i
].getCell(x
, y
);
1861 // Clears all coordMaps
1863 var coordMaps
= this.coordMaps
;
1866 for (i
= 0; i
< coordMaps
.length
; i
++) {
1867 coordMaps
[i
].clear();
1873 /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
1874 ----------------------------------------------------------------------------------------------------------------------*/
1875 // TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)
1877 var DragListener
= Class
.extend({
1885 // the cell the mouse was over when listening started
1888 // the cell the mouse is over
1891 // coordinates of the initial mousedown
1895 // handler attached to the document, bound to the DragListener's `this`
1896 mousemoveProxy
: null,
1900 scrollBounds
: null, // { top, bottom, left, right }
1901 scrollTopVel
: null, // pixels per second
1902 scrollLeftVel
: null, // pixels per second
1903 scrollIntervalId
: null, // ID of setTimeout for scrolling animation loop
1904 scrollHandlerProxy
: null, // this-scoped function for handling when scrollEl is scrolled
1906 scrollSensitivity
: 30, // pixels from edge for scrolling to start
1907 scrollSpeed
: 200, // pixels per second, at maximum speed
1908 scrollIntervalMs
: 50, // millisecond wait between scroll increment
1911 constructor: function(coordMap
, options
) {
1912 this.coordMap
= coordMap
;
1913 this.options
= options
|| {};
1917 // Call this when the user does a mousedown. Will probably lead to startListening
1918 mousedown: function(ev
) {
1919 if (isPrimaryMouseButton(ev
)) {
1921 ev
.preventDefault(); // prevents native selection in most browsers
1923 this.startListening(ev
);
1925 // start the drag immediately if there is no minimum distance for a drag start
1926 if (!this.options
.distance
) {
1933 // Call this to start tracking mouse movements
1934 startListening: function(ev
) {
1938 if (!this.isListening
) {
1940 // grab scroll container and attach handler
1941 if (ev
&& this.options
.scroll
) {
1942 scrollParent
= getScrollParent($(ev
.target
));
1943 if (!scrollParent
.is(window
) && !scrollParent
.is(document
)) {
1944 this.scrollEl
= scrollParent
;
1946 // scope to `this`, and use `debounce` to make sure rapid calls don't happen
1947 this.scrollHandlerProxy
= debounce($.proxy(this, 'scrollHandler'), 100);
1948 this.scrollEl
.on('scroll', this.scrollHandlerProxy
);
1952 this.computeCoords(); // relies on `scrollEl`
1954 // get info on the initial cell and its coordinates
1956 cell
= this.getCell(ev
);
1957 this.origCell
= cell
;
1959 this.mouseX0
= ev
.pageX
;
1960 this.mouseY0
= ev
.pageY
;
1964 .on('mousemove', this.mousemoveProxy
= $.proxy(this, 'mousemove'))
1965 .on('mouseup', this.mouseupProxy
= $.proxy(this, 'mouseup'))
1966 .on('selectstart', this.preventDefault
); // prevents native selection in IE<=8
1968 this.isListening
= true;
1969 this.trigger('listenStart', ev
);
1974 // Recomputes the drag-critical positions of elements
1975 computeCoords: function() {
1976 this.coordMap
.build();
1977 this.computeScrollBounds();
1981 // Called when the user moves the mouse
1982 mousemove: function(ev
) {
1984 var distanceSq
; // current distance from mouseX0/mouseY0, squared
1986 if (!this.isDragging
) { // if not already dragging...
1987 // then start the drag if the minimum distance criteria is met
1988 minDistance
= this.options
.distance
|| 1;
1989 distanceSq
= Math
.pow(ev
.pageX
- this.mouseX0
, 2) + Math
.pow(ev
.pageY
- this.mouseY0
, 2);
1990 if (distanceSq
>= minDistance
* minDistance
) { // use pythagorean theorem
1995 if (this.isDragging
) {
1996 this.drag(ev
); // report a drag, even if this mousemove initiated the drag
2001 // Call this to initiate a legitimate drag.
2002 // This function is called internally from this class, but can also be called explicitly from outside
2003 startDrag: function(ev
) {
2006 if (!this.isListening
) { // startDrag must have manually initiated
2007 this.startListening();
2010 if (!this.isDragging
) {
2011 this.isDragging
= true;
2012 this.trigger('dragStart', ev
);
2014 // report the initial cell the mouse is over
2015 // especially important if no min-distance and drag starts immediately
2016 cell
= this.getCell(ev
); // this might be different from this.origCell if the min-distance is large
2018 this.cellOver(cell
);
2024 // Called while the mouse is being moved and when we know a legitimate drag is taking place
2025 drag: function(ev
) {
2028 if (this.isDragging
) {
2029 cell
= this.getCell(ev
);
2031 if (!isCellsEqual(cell
, this.cell
)) { // a different cell than before?
2036 this.cellOver(cell
);
2040 this.dragScroll(ev
); // will possibly cause scrolling
2045 // Called when a the mouse has just moved over a new cell
2046 cellOver: function(cell
) {
2048 this.trigger('cellOver', cell
, isCellsEqual(cell
, this.origCell
));
2052 // Called when the mouse has just moved out of a cell
2053 cellOut: function() {
2055 this.trigger('cellOut', this.cell
);
2061 // Called when the user does a mouseup
2062 mouseup: function(ev
) {
2064 this.stopListening(ev
);
2068 // Called when the drag is over. Will not cause listening to stop however.
2069 // A concluding 'cellOut' event will NOT be triggered.
2070 stopDrag: function(ev
) {
2071 if (this.isDragging
) {
2072 this.stopScrolling();
2073 this.trigger('dragStop', ev
);
2074 this.isDragging
= false;
2079 // Call this to stop listening to the user's mouse events
2080 stopListening: function(ev
) {
2081 if (this.isListening
) {
2083 // remove the scroll handler if there is a scrollEl
2084 if (this.scrollEl
) {
2085 this.scrollEl
.off('scroll', this.scrollHandlerProxy
);
2086 this.scrollHandlerProxy
= null;
2090 .off('mousemove', this.mousemoveProxy
)
2091 .off('mouseup', this.mouseupProxy
)
2092 .off('selectstart', this.preventDefault
);
2094 this.mousemoveProxy
= null;
2095 this.mouseupProxy
= null;
2097 this.isListening
= false;
2098 this.trigger('listenStop', ev
);
2100 this.origCell
= this.cell
= null;
2101 this.coordMap
.clear();
2106 // Gets the cell underneath the coordinates for the given mouse event
2107 getCell: function(ev
) {
2108 return this.coordMap
.getCell(ev
.pageX
, ev
.pageY
);
2112 // Triggers a callback. Calls a function in the option hash of the same name.
2113 // Arguments beyond the first `name` are forwarded on.
2114 trigger: function(name
) {
2115 if (this.options
[name
]) {
2116 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
2121 // Stops a given mouse event from doing it's native browser action. In our case, text selection.
2122 preventDefault: function(ev
) {
2123 ev
.preventDefault();
2128 ------------------------------------------------------------------------------------------------------------------*/
2131 // Computes and stores the bounding rectangle of scrollEl
2132 computeScrollBounds: function() {
2133 var el
= this.scrollEl
;
2137 offset
= el
.offset();
2138 this.scrollBounds
= {
2141 bottom
: offset
.top
+ el
.outerHeight(),
2142 right
: offset
.left
+ el
.outerWidth()
2148 // Called when the dragging is in progress and scrolling should be updated
2149 dragScroll: function(ev
) {
2150 var sensitivity
= this.scrollSensitivity
;
2151 var bounds
= this.scrollBounds
;
2152 var topCloseness
, bottomCloseness
;
2153 var leftCloseness
, rightCloseness
;
2157 if (bounds
) { // only scroll if scrollEl exists
2159 // compute closeness to edges. valid range is from 0.0 - 1.0
2160 topCloseness
= (sensitivity
- (ev
.pageY
- bounds
.top
)) / sensitivity
;
2161 bottomCloseness
= (sensitivity
- (bounds
.bottom
- ev
.pageY
)) / sensitivity
;
2162 leftCloseness
= (sensitivity
- (ev
.pageX
- bounds
.left
)) / sensitivity
;
2163 rightCloseness
= (sensitivity
- (bounds
.right
- ev
.pageX
)) / sensitivity
;
2165 // translate vertical closeness into velocity.
2166 // mouse must be completely in bounds for velocity to happen.
2167 if (topCloseness
>= 0 && topCloseness
<= 1) {
2168 topVel
= topCloseness
* this.scrollSpeed
* -1; // negative. for scrolling up
2170 else if (bottomCloseness
>= 0 && bottomCloseness
<= 1) {
2171 topVel
= bottomCloseness
* this.scrollSpeed
;
2174 // translate horizontal closeness into velocity
2175 if (leftCloseness
>= 0 && leftCloseness
<= 1) {
2176 leftVel
= leftCloseness
* this.scrollSpeed
* -1; // negative. for scrolling left
2178 else if (rightCloseness
>= 0 && rightCloseness
<= 1) {
2179 leftVel
= rightCloseness
* this.scrollSpeed
;
2183 this.setScrollVel(topVel
, leftVel
);
2187 // Sets the speed-of-scrolling for the scrollEl
2188 setScrollVel: function(topVel
, leftVel
) {
2190 this.scrollTopVel
= topVel
;
2191 this.scrollLeftVel
= leftVel
;
2193 this.constrainScrollVel(); // massages into realistic values
2195 // if there is non-zero velocity, and an animation loop hasn't already started, then START
2196 if ((this.scrollTopVel
|| this.scrollLeftVel
) && !this.scrollIntervalId
) {
2197 this.scrollIntervalId
= setInterval(
2198 $.proxy(this, 'scrollIntervalFunc'), // scope to `this`
2199 this.scrollIntervalMs
2205 // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
2206 constrainScrollVel: function() {
2207 var el
= this.scrollEl
;
2209 if (this.scrollTopVel
< 0) { // scrolling up?
2210 if (el
.scrollTop() <= 0) { // already scrolled all the way up?
2211 this.scrollTopVel
= 0;
2214 else if (this.scrollTopVel
> 0) { // scrolling down?
2215 if (el
.scrollTop() + el
[0].clientHeight
>= el
[0].scrollHeight
) { // already scrolled all the way down?
2216 this.scrollTopVel
= 0;
2220 if (this.scrollLeftVel
< 0) { // scrolling left?
2221 if (el
.scrollLeft() <= 0) { // already scrolled all the left?
2222 this.scrollLeftVel
= 0;
2225 else if (this.scrollLeftVel
> 0) { // scrolling right?
2226 if (el
.scrollLeft() + el
[0].clientWidth
>= el
[0].scrollWidth
) { // already scrolled all the way right?
2227 this.scrollLeftVel
= 0;
2233 // This function gets called during every iteration of the scrolling animation loop
2234 scrollIntervalFunc: function() {
2235 var el
= this.scrollEl
;
2236 var frac
= this.scrollIntervalMs
/ 1000; // considering animation frequency, what the vel should be mult'd by
2238 // change the value of scrollEl's scroll
2239 if (this.scrollTopVel
) {
2240 el
.scrollTop(el
.scrollTop() + this.scrollTopVel
* frac
);
2242 if (this.scrollLeftVel
) {
2243 el
.scrollLeft(el
.scrollLeft() + this.scrollLeftVel
* frac
);
2246 this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
2248 // if scrolled all the way, which causes the vels to be zero, stop the animation loop
2249 if (!this.scrollTopVel
&& !this.scrollLeftVel
) {
2250 this.stopScrolling();
2255 // Kills any existing scrolling animation loop
2256 stopScrolling: function() {
2257 if (this.scrollIntervalId
) {
2258 clearInterval(this.scrollIntervalId
);
2259 this.scrollIntervalId
= null;
2261 // when all done with scrolling, recompute positions since they probably changed
2262 this.computeCoords();
2267 // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
2268 scrollHandler: function() {
2269 // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
2270 if (!this.scrollIntervalId
) {
2271 this.computeCoords();
2278 // Returns `true` if the cells are identically equal. `false` otherwise.
2279 // They must have the same row, col, and be from the same grid.
2280 // Two null values will be considered equal, as two "out of the grid" states are the same.
2281 function isCellsEqual(cell1
, cell2
) {
2283 if (!cell1
&& !cell2
) {
2287 if (cell1
&& cell2
) {
2288 return cell1
.grid
=== cell2
.grid
&&
2289 cell1
.row
=== cell2
.row
&&
2290 cell1
.col
=== cell2
.col
;
2296 /* Creates a clone of an element and lets it track the mouse as it moves
2297 ----------------------------------------------------------------------------------------------------------------------*/
2299 var MouseFollower
= Class
.extend({
2303 sourceEl
: null, // the element that will be cloned and made to look like it is dragging
2304 el
: null, // the clone of `sourceEl` that will track the mouse
2305 parentEl
: null, // the element that `el` (the clone) will be attached to
2307 // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
2311 // the initial position of the mouse
2315 // the number of pixels the mouse has moved from its initial position
2319 mousemoveProxy
: null, // document mousemove handler, bound to the MouseFollower's `this`
2323 isAnimating
: false, // doing the revert animation?
2325 constructor: function(sourceEl
, options
) {
2326 this.options
= options
= options
|| {};
2327 this.sourceEl
= sourceEl
;
2328 this.parentEl
= options
.parentEl
? $(options
.parentEl
) : sourceEl
.parent(); // default to sourceEl's parent
2332 // Causes the element to start following the mouse
2333 start: function(ev
) {
2334 if (!this.isFollowing
) {
2335 this.isFollowing
= true;
2337 this.mouseY0
= ev
.pageY
;
2338 this.mouseX0
= ev
.pageX
;
2342 if (!this.isHidden
) {
2343 this.updatePosition();
2346 $(document
).on('mousemove', this.mousemoveProxy
= $.proxy(this, 'mousemove'));
2351 // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
2352 // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
2353 stop: function(shouldRevert
, callback
) {
2355 var revertDuration
= this.options
.revertDuration
;
2357 function complete() {
2358 this.isAnimating
= false;
2361 this.top0
= this.left0
= null; // reset state for future updatePosition calls
2368 if (this.isFollowing
&& !this.isAnimating
) { // disallow more than one stop animation at a time
2369 this.isFollowing
= false;
2371 $(document
).off('mousemove', this.mousemoveProxy
);
2373 if (shouldRevert
&& revertDuration
&& !this.isHidden
) { // do a revert animation?
2374 this.isAnimating
= true;
2379 duration
: revertDuration
,
2390 // Gets the tracking element. Create it if necessary
2395 this.sourceEl
.width(); // hack to force IE8 to compute correct bounding box
2396 el
= this.el
= this.sourceEl
.clone()
2398 position
: 'absolute',
2399 visibility
: '', // in case original element was hidden (commonly through hideEvents())
2400 display
: this.isHidden
? 'none' : '', // for when initially hidden
2402 right
: 'auto', // erase and set width instead
2403 bottom
: 'auto', // erase and set height instead
2404 width
: this.sourceEl
.width(), // explicit height in case there was a 'right' value
2405 height
: this.sourceEl
.height(), // explicit width in case there was a 'bottom' value
2406 opacity
: this.options
.opacity
|| '',
2407 zIndex
: this.options
.zIndex
2409 .appendTo(this.parentEl
);
2416 // Removes the tracking element if it has already been created
2417 destroyEl: function() {
2425 // Update the CSS position of the tracking element
2426 updatePosition: function() {
2430 this.getEl(); // ensure this.el
2432 // make sure origin info was computed
2433 if (this.top0
=== null) {
2434 this.sourceEl
.width(); // hack to force IE8 to compute correct bounding box
2435 sourceOffset
= this.sourceEl
.offset();
2436 origin
= this.el
.offsetParent().offset();
2437 this.top0
= sourceOffset
.top
- origin
.top
;
2438 this.left0
= sourceOffset
.left
- origin
.left
;
2442 top
: this.top0
+ this.topDelta
,
2443 left
: this.left0
+ this.leftDelta
2448 // Gets called when the user moves the mouse
2449 mousemove: function(ev
) {
2450 this.topDelta
= ev
.pageY
- this.mouseY0
;
2451 this.leftDelta
= ev
.pageX
- this.mouseX0
;
2453 if (!this.isHidden
) {
2454 this.updatePosition();
2459 // Temporarily makes the tracking element invisible. Can be called before following starts
2461 if (!this.isHidden
) {
2462 this.isHidden
= true;
2470 // Show the tracking element after it has been temporarily hidden
2472 if (this.isHidden
) {
2473 this.isHidden
= false;
2474 this.updatePosition();
2475 this.getEl().show();
2481 /* A utility class for rendering <tr> rows.
2482 ----------------------------------------------------------------------------------------------------------------------*/
2483 // It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
2484 // (such as highlight rows, day rows, helper rows, etc).
2486 var RowRenderer
= Class
.extend({
2488 view
: null, // a View object
2489 isRTL
: null, // shortcut to the view's isRTL option
2490 cellHtml
: '<td/>', // plain default HTML used for a cell when no other is available
2493 constructor: function(view
) {
2495 this.isRTL
= view
.opt('isRTL');
2499 // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
2500 // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
2501 // `row` is an optional row number.
2502 rowHtml: function(rowType
, row
) {
2503 var renderCell
= this.getHtmlRenderer('cell', rowType
);
2504 var rowCellHtml
= '';
2510 for (col
= 0; col
< this.colCnt
; col
++) {
2511 cell
= this.getCell(row
, col
);
2512 rowCellHtml
+= renderCell(cell
);
2515 rowCellHtml
= this.bookendCells(rowCellHtml
, rowType
, row
); // apply intro and outro
2517 return '<tr>' + rowCellHtml
+ '</tr>';
2521 // Applies the "intro" and "outro" HTML to the given cells.
2522 // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
2523 // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
2524 // `row` is an optional row number.
2525 bookendCells: function(cells
, rowType
, row
) {
2526 var intro
= this.getHtmlRenderer('intro', rowType
)(row
|| 0);
2527 var outro
= this.getHtmlRenderer('outro', rowType
)(row
|| 0);
2528 var prependHtml
= this.isRTL
? outro
: intro
;
2529 var appendHtml
= this.isRTL
? intro
: outro
;
2531 if (typeof cells
=== 'string') {
2532 return prependHtml
+ cells
+ appendHtml
;
2534 else { // a jQuery <tr> element
2535 return cells
.prepend(prependHtml
).append(appendHtml
);
2540 // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
2541 // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
2542 // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
2543 // We will query the View object first for any custom rendering functions, then the methods of the subclass.
2544 getHtmlRenderer: function(rendererName
, rowType
) {
2545 var view
= this.view
;
2546 var generalName
; // like "cellHtml"
2547 var specificName
; // like "dayCellHtml". based on rowType
2548 var provider
; // either the View or the RowRenderer subclass, whichever provided the method
2551 generalName
= rendererName
+ 'Html';
2553 specificName
= rowType
+ capitaliseFirstLetter(rendererName
) + 'Html';
2556 if (specificName
&& (renderer
= view
[specificName
])) {
2559 else if (specificName
&& (renderer
= this[specificName
])) {
2562 else if ((renderer
= view
[generalName
])) {
2565 else if ((renderer
= this[generalName
])) {
2569 if (typeof renderer
=== 'function') {
2571 return renderer
.apply(provider
, arguments
) || ''; // use correct `this` and always return a string
2575 // the rendered can be a plain string as well. if not specified, always an empty string.
2577 return renderer
|| '';
2583 /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
2584 ----------------------------------------------------------------------------------------------------------------------*/
2586 var Grid
= fc
.Grid
= RowRenderer
.extend({
2588 start
: null, // the date of the first cell
2589 end
: null, // the date after the last cell
2591 rowCnt
: 0, // number of rows
2592 colCnt
: 0, // number of cols
2593 rowData
: null, // array of objects, holding misc data for each row
2594 colData
: null, // array of objects, holding misc data for each column
2596 el
: null, // the containing element
2597 coordMap
: null, // a GridCoordMap that converts pixel values to datetimes
2598 elsByFill
: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
2600 documentDragStartProxy
: null, // binds the Grid's scope to documentDragStart (in DayGrid.events)
2602 // derived from options
2603 colHeadFormat
: null, // TODO: move to another class. not applicable to all Grids
2604 eventTimeFormat
: null,
2605 displayEventEnd
: null,
2608 constructor: function() {
2609 RowRenderer
.apply(this, arguments
); // call the super-constructor
2611 this.coordMap
= new GridCoordMap(this);
2612 this.elsByFill
= {};
2613 this.documentDragStartProxy
= $.proxy(this, 'documentDragStart');
2617 // Renders the grid into the `el` element.
2618 // Subclasses should override and call this super-method when done.
2619 render: function() {
2620 this.bindHandlers();
2624 // Called when the grid's resources need to be cleaned up
2625 destroy: function() {
2626 this.unbindHandlers();
2631 ------------------------------------------------------------------------------------------------------------------*/
2634 // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
2635 // TODO: move to another class. not applicable to all Grids
2636 computeColHeadFormat: function() {
2637 // subclasses must implement if they want to use headHtml()
2641 // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
2642 computeEventTimeFormat: function() {
2643 return this.view
.opt('smallTimeFormat');
2647 // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
2648 computeDisplayEventEnd: function() {
2654 ------------------------------------------------------------------------------------------------------------------*/
2657 // Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system.
2658 setRange: function(range
) {
2659 var view
= this.view
;
2661 this.start
= range
.start
.clone();
2662 this.end
= range
.end
.clone();
2668 // Populate option-derived settings. Look for override first, then compute if necessary.
2669 this.colHeadFormat
= view
.opt('columnFormat') || this.computeColHeadFormat();
2670 this.eventTimeFormat
= view
.opt('timeFormat') || this.computeEventTimeFormat();
2671 this.displayEventEnd
= view
.opt('displayEventEnd');
2672 if (this.displayEventEnd
== null) {
2673 this.displayEventEnd
= this.computeDisplayEventEnd();
2678 // Responsible for setting rowCnt/colCnt and any other row/col data
2679 updateCells: function() {
2680 // subclasses must implement
2684 // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
2685 rangeToSegs: function(range
) {
2686 // subclasses must implement
2691 ------------------------------------------------------------------------------------------------------------------*/
2692 // NOTE: columns are ordered left-to-right
2695 // Gets an object containing row/col number, misc data, and range information about the cell.
2696 // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
2697 getCell: function(row
, col
) {
2701 if (typeof row
=== 'number') { // a single-number offset
2702 col
= row
% this.colCnt
;
2703 row
= Math
.floor(row
/ this.colCnt
);
2705 else { // an object with row/col properties
2711 cell
= { row
: row
, col
: col
};
2713 $.extend(cell
, this.getRowData(row
), this.getColData(col
));
2714 $.extend(cell
, this.computeCellRange(cell
));
2720 // Given a cell object with index and misc data, generates a range object
2721 computeCellRange: function(cell
) {
2722 // subclasses must implement
2726 // Retrieves misc data about the given row
2727 getRowData: function(row
) {
2728 return this.rowData
[row
] || {};
2732 // Retrieves misc data baout the given column
2733 getColData: function(col
) {
2734 return this.colData
[col
] || {};
2738 // Retrieves the element representing the given row
2739 getRowEl: function(row
) {
2740 // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
2744 // Retrieves the element representing the given column
2745 getColEl: function(col
) {
2746 // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
2750 // Given a cell object, returns the element that represents the cell's whole-day
2751 getCellDayEl: function(cell
) {
2752 return this.getColEl(cell
.col
) || this.getRowEl(cell
.row
);
2757 ------------------------------------------------------------------------------------------------------------------*/
2760 // Computes the top/bottom coordinates of all rows.
2761 // By default, queries the dimensions of the element provided by getRowEl().
2762 computeRowCoords: function() {
2767 for (i
= 0; i
< this.rowCnt
; i
++) {
2768 el
= this.getRowEl(i
);
2770 top
: el
.offset().top
2773 items
[i
- 1].bottom
= item
.top
;
2777 item
.bottom
= item
.top
+ el
.outerHeight();
2783 // Computes the left/right coordinates of all rows.
2784 // By default, queries the dimensions of the element provided by getColEl().
2785 computeColCoords: function() {
2790 for (i
= 0; i
< this.colCnt
; i
++) {
2791 el
= this.getColEl(i
);
2793 left
: el
.offset().left
2796 items
[i
- 1].right
= item
.left
;
2800 item
.right
= item
.left
+ el
.outerWidth();
2807 ------------------------------------------------------------------------------------------------------------------*/
2810 // Attaches handlers to DOM
2811 bindHandlers: function() {
2814 // attach a handler to the grid's root element.
2815 // we don't need to clean up in unbindHandlers or destroy, because when jQuery removes the element from the
2816 // DOM it automatically unregisters the handlers.
2817 this.el
.on('mousedown', function(ev
) {
2819 !$(ev
.target
).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
2820 !$(ev
.target
).closest('.fc-popover').length
// not on a popover (like the "more.." events one)
2822 _this
.dayMousedown(ev
);
2826 // attach event-element-related handlers. in Grid.events
2827 // same garbage collection note as above.
2828 this.bindSegHandlers();
2830 $(document
).on('dragstart', this.documentDragStartProxy
); // jqui drag
2834 // Unattaches handlers from the DOM
2835 unbindHandlers: function() {
2836 $(document
).off('dragstart', this.documentDragStartProxy
); // jqui drag
2840 // Process a mousedown on an element that represents a day. For day clicking and selecting.
2841 dayMousedown: function(ev
) {
2843 var view
= this.view
;
2844 var isSelectable
= view
.opt('selectable');
2845 var dayClickCell
; // null if invalid dayClick
2846 var selectionRange
; // null if invalid selection
2848 // this listener tracks a mousedown on a day element, and a subsequent drag.
2849 // if the drag ends on the same day, it is a 'dayClick'.
2850 // if 'selectable' is enabled, this listener also detects selections.
2851 var dragListener
= new DragListener(this.coordMap
, {
2852 //distance: 5, // needs more work if we want dayClick to fire correctly
2853 scroll
: view
.opt('dragScroll'),
2854 dragStart: function() {
2855 view
.unselect(); // since we could be rendering a new selection, we want to clear any old one
2857 cellOver: function(cell
, isOrig
) {
2858 var origCell
= dragListener
.origCell
;
2859 if (origCell
) { // click needs to have started on a cell
2860 dayClickCell
= isOrig
? cell
: null; // single-cell selection is a day click
2862 selectionRange
= _this
.computeSelection(origCell
, cell
);
2863 if (selectionRange
) {
2864 _this
.renderSelection(selectionRange
);
2872 cellOut: function(cell
) {
2873 dayClickCell
= null;
2874 selectionRange
= null;
2875 _this
.destroySelection();
2878 listenStop: function(ev
) {
2880 view
.trigger('dayClick', _this
.getCellDayEl(dayClickCell
), dayClickCell
.start
, ev
);
2882 if (selectionRange
) {
2883 // the selection will already have been rendered. just report it
2884 view
.reportSelection(selectionRange
, ev
);
2890 dragListener
.mousedown(ev
); // start listening, which will eventually initiate a dragStart
2895 ------------------------------------------------------------------------------------------------------------------*/
2896 // TODO: should probably move this to Grid.events, like we did event dragging / resizing
2899 // Renders a mock event over the given range.
2900 // The range's end can be null, in which case the mock event that is rendered will have a null end time.
2901 // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
2902 renderRangeHelper: function(range
, sourceSeg
) {
2905 fakeEvent
= sourceSeg
? createObject(sourceSeg
.event
) : {}; // mask the original event object if possible
2906 fakeEvent
.start
= range
.start
.clone();
2907 fakeEvent
.end
= range
.end
? range
.end
.clone() : null;
2908 fakeEvent
.allDay
= null; // force it to be freshly computed by normalizeEventDateProps
2909 this.view
.calendar
.normalizeEventDateProps(fakeEvent
);
2911 // this extra className will be useful for differentiating real events from mock events in CSS
2912 fakeEvent
.className
= (fakeEvent
.className
|| []).concat('fc-helper');
2914 // if something external is being dragged in, don't render a resizer
2916 fakeEvent
.editable
= false;
2919 this.renderHelper(fakeEvent
, sourceSeg
); // do the actual rendering
2923 // Renders a mock event
2924 renderHelper: function(event
, sourceSeg
) {
2925 // subclasses must implement
2929 // Unrenders a mock event
2930 destroyHelper: function() {
2931 // subclasses must implement
2936 ------------------------------------------------------------------------------------------------------------------*/
2939 // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
2940 renderSelection: function(range
) {
2941 this.renderHighlight(range
);
2945 // Unrenders any visual indications of a selection. Will unrender a highlight by default.
2946 destroySelection: function() {
2947 this.destroyHighlight();
2951 // Given the first and last cells of a selection, returns a range object.
2952 // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
2953 // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
2954 computeSelection: function(firstCell
, lastCell
) {
2963 dates
.sort(compareNumbers
); // sorts chronologically. works with Moments
2966 start
: dates
[0].clone(),
2967 end
: dates
[3].clone()
2970 if (!this.view
.calendar
.isSelectionRangeAllowed(range
)) {
2979 ------------------------------------------------------------------------------------------------------------------*/
2982 // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
2983 renderHighlight: function(range
) {
2984 this.renderFill('highlight', this.rangeToSegs(range
));
2988 // Unrenders the emphasis on a date range
2989 destroyHighlight: function() {
2990 this.destroyFill('highlight');
2994 // Generates an array of classNames for rendering the highlight. Used by the fill system.
2995 highlightSegClasses: function() {
2996 return [ 'fc-highlight' ];
3000 /* Fill System (highlight, background events, business hours)
3001 ------------------------------------------------------------------------------------------------------------------*/
3004 // Renders a set of rectangles over the given segments of time.
3005 // Returns a subset of segs, the segs that were actually rendered.
3006 // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
3007 renderFill: function(type
, segs
) {
3008 // subclasses must implement
3012 // Unrenders a specific type of fill that is currently rendered on the grid
3013 destroyFill: function(type
) {
3014 var el
= this.elsByFill
[type
];
3018 delete this.elsByFill
[type
];
3023 // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
3024 // Only returns segments that successfully rendered.
3025 // To be harnessed by renderFill (implemented by subclasses).
3026 // Analagous to renderFgSegEls.
3027 renderFillSegEls: function(type
, segs
) {
3029 var segElMethod
= this[type
+ 'SegEl'];
3031 var renderedSegs
= [];
3036 // build a large concatenation of segment HTML
3037 for (i
= 0; i
< segs
.length
; i
++) {
3038 html
+= this.fillSegHtml(type
, segs
[i
]);
3041 // Grab individual elements from the combined HTML string. Use each as the default rendering.
3042 // Then, compute the 'el' for each segment.
3043 $(html
).each(function(i
, node
) {
3047 // allow custom filter methods per-type
3049 el
= segElMethod
.call(_this
, seg
, el
);
3052 if (el
) { // custom filters did not cancel the render
3053 el
= $(el
); // allow custom filter to return raw DOM node
3055 // correct element type? (would be bad if a non-TD were inserted into a table for example)
3056 if (el
.is(_this
.fillSegTag
)) {
3058 renderedSegs
.push(seg
);
3064 return renderedSegs
;
3068 fillSegTag
: 'div', // subclasses can override
3071 // Builds the HTML needed for one fill segment. Generic enought o work with different types.
3072 fillSegHtml: function(type
, seg
) {
3073 var classesMethod
= this[type
+ 'SegClasses']; // custom hooks per-type
3074 var stylesMethod
= this[type
+ 'SegStyles']; //
3075 var classes
= classesMethod
? classesMethod
.call(this, seg
) : [];
3076 var styles
= stylesMethod
? stylesMethod
.call(this, seg
) : ''; // a semi-colon separated CSS property string
3078 return '<' + this.fillSegTag
+
3079 (classes
.length
? ' class="' + classes
.join(' ') + '"' : '') +
3080 (styles
? ' style="' + styles
+ '"' : '') +
3085 /* Generic rendering utilities for subclasses
3086 ------------------------------------------------------------------------------------------------------------------*/
3089 // Renders a day-of-week header row.
3090 // TODO: move to another class. not applicable to all Grids
3091 headHtml: function() {
3093 '<div class="fc-row ' + this.view
.widgetHeaderClass
+ '">' +
3096 this.rowHtml('head') + // leverages RowRenderer
3103 // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
3104 // TODO: move to another class. not applicable to all Grids
3105 headCellHtml: function(cell
) {
3106 var view
= this.view
;
3107 var date
= cell
.start
;
3110 '<th class="fc-day-header ' + view
.widgetHeaderClass
+ ' fc-' + dayIDs
[date
.day()] + '">' +
3111 htmlEscape(date
.format(this.colHeadFormat
)) +
3116 // Renders the HTML for a single-day background cell
3117 bgCellHtml: function(cell
) {
3118 var view
= this.view
;
3119 var date
= cell
.start
;
3120 var classes
= this.getDayClasses(date
);
3122 classes
.unshift('fc-day', view
.widgetContentClass
);
3124 return '<td class="' + classes
.join(' ') + '"' +
3125 ' data-date="' + date
.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
3130 // Computes HTML classNames for a single-day cell
3131 getDayClasses: function(date
) {
3132 var view
= this.view
;
3133 var today
= view
.calendar
.getNow().stripTime();
3134 var classes
= [ 'fc-' + dayIDs
[date
.day()] ];
3137 view
.name
=== 'month' &&
3138 date
.month() != view
.intervalStart
.month()
3140 classes
.push('fc-other-month');
3143 if (date
.isSame(today
, 'day')) {
3146 view
.highlightStateClass
3149 else if (date
< today
) {
3150 classes
.push('fc-past');
3153 classes
.push('fc-future');
3161 /* Event-rendering and event-interaction methods for the abstract Grid class
3162 ----------------------------------------------------------------------------------------------------------------------*/
3166 mousedOverSeg
: null, // the segment object the user's mouse is over. null if over nothing
3167 isDraggingSeg
: false, // is a segment being dragged? boolean
3168 isResizingSeg
: false, // is a segment being resized? boolean
3169 segs
: null, // the event segments currently rendered in the grid
3172 // Renders the given events onto the grid
3173 renderEvents: function(events
) {
3174 var segs
= this.eventsToSegs(events
);
3179 for (i
= 0; i
< segs
.length
; i
++) {
3182 if (isBgEvent(seg
.event
)) {
3190 // Render each different type of segment.
3191 // Each function may return a subset of the segs, segs that were actually rendered.
3192 bgSegs
= this.renderBgSegs(bgSegs
) || bgSegs
;
3193 fgSegs
= this.renderFgSegs(fgSegs
) || fgSegs
;
3195 this.segs
= bgSegs
.concat(fgSegs
);
3199 // Unrenders all events currently rendered on the grid
3200 destroyEvents: function() {
3201 this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
3203 this.destroyFgSegs();
3204 this.destroyBgSegs();
3210 // Retrieves all rendered segment objects currently rendered on the grid
3211 getEventSegs: function() {
3212 return this.segs
|| [];
3216 /* Foreground Segment Rendering
3217 ------------------------------------------------------------------------------------------------------------------*/
3220 // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
3221 renderFgSegs: function(segs
) {
3222 // subclasses must implement
3226 // Unrenders all currently rendered foreground segments
3227 destroyFgSegs: function() {
3228 // subclasses must implement
3232 // Renders and assigns an `el` property for each foreground event segment.
3233 // Only returns segments that successfully rendered.
3234 // A utility that subclasses may use.
3235 renderFgSegEls: function(segs
, disableResizing
) {
3236 var view
= this.view
;
3238 var renderedSegs
= [];
3241 if (segs
.length
) { // don't build an empty html string
3243 // build a large concatenation of event segment HTML
3244 for (i
= 0; i
< segs
.length
; i
++) {
3245 html
+= this.fgSegHtml(segs
[i
], disableResizing
);
3248 // Grab individual elements from the combined HTML string. Use each as the default rendering.
3249 // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
3250 $(html
).each(function(i
, node
) {
3252 var el
= view
.resolveEventEl(seg
.event
, $(node
));
3255 el
.data('fc-seg', seg
); // used by handlers
3257 renderedSegs
.push(seg
);
3262 return renderedSegs
;
3266 // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
3267 fgSegHtml: function(seg
, disableResizing
) {
3268 // subclasses should implement
3272 /* Background Segment Rendering
3273 ------------------------------------------------------------------------------------------------------------------*/
3276 // Renders the given background event segments onto the grid.
3277 // Returns a subset of the segs that were actually rendered.
3278 renderBgSegs: function(segs
) {
3279 return this.renderFill('bgEvent', segs
);
3283 // Unrenders all the currently rendered background event segments
3284 destroyBgSegs: function() {
3285 this.destroyFill('bgEvent');
3289 // Renders a background event element, given the default rendering. Called by the fill system.
3290 bgEventSegEl: function(seg
, el
) {
3291 return this.view
.resolveEventEl(seg
.event
, el
); // will filter through eventRender
3295 // Generates an array of classNames to be used for the default rendering of a background event.
3296 // Called by the fill system.
3297 bgEventSegClasses: function(seg
) {
3298 var event
= seg
.event
;
3299 var source
= event
.source
|| {};
3301 return [ 'fc-bgevent' ].concat(
3303 source
.className
|| []
3308 // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
3309 // Called by the fill system.
3310 // TODO: consolidate with getEventSkinCss?
3311 bgEventSegStyles: function(seg
) {
3312 var view
= this.view
;
3313 var event
= seg
.event
;
3314 var source
= event
.source
|| {};
3315 var eventColor
= event
.color
;
3316 var sourceColor
= source
.color
;
3317 var optionColor
= view
.opt('eventColor');
3318 var backgroundColor
=
3319 event
.backgroundColor
||
3321 source
.backgroundColor
||
3323 view
.opt('eventBackgroundColor') ||
3326 if (backgroundColor
) {
3327 return 'background-color:' + backgroundColor
;
3334 // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
3335 businessHoursSegClasses: function(seg
) {
3336 return [ 'fc-nonbusiness', 'fc-bgevent' ];
3341 ------------------------------------------------------------------------------------------------------------------*/
3344 // Attaches event-element-related handlers to the container element and leverage bubbling
3345 bindSegHandlers: function() {
3347 var view
= this.view
;
3351 mouseenter: function(seg
, ev
) {
3352 _this
.triggerSegMouseover(seg
, ev
);
3354 mouseleave: function(seg
, ev
) {
3355 _this
.triggerSegMouseout(seg
, ev
);
3357 click: function(seg
, ev
) {
3358 return view
.trigger('eventClick', this, seg
.event
, ev
); // can return `false` to cancel
3360 mousedown: function(seg
, ev
) {
3361 if ($(ev
.target
).is('.fc-resizer') && view
.isEventResizable(seg
.event
)) {
3362 _this
.segResizeMousedown(seg
, ev
);
3364 else if (view
.isEventDraggable(seg
.event
)) {
3365 _this
.segDragMousedown(seg
, ev
);
3369 function(name
, func
) {
3370 // attach the handler to the container element and only listen for real event elements via bubbling
3371 _this
.el
.on(name
, '.fc-event-container > *', function(ev
) {
3372 var seg
= $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
3374 // only call the handlers if there is not a drag/resize in progress
3375 if (seg
&& !_this
.isDraggingSeg
&& !_this
.isResizingSeg
) {
3376 return func
.call(this, seg
, ev
); // `this` will be the event element
3384 // Updates internal state and triggers handlers for when an event element is moused over
3385 triggerSegMouseover: function(seg
, ev
) {
3386 if (!this.mousedOverSeg
) {
3387 this.mousedOverSeg
= seg
;
3388 this.view
.trigger('eventMouseover', seg
.el
[0], seg
.event
, ev
);
3393 // Updates internal state and triggers handlers for when an event element is moused out.
3394 // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
3395 triggerSegMouseout: function(seg
, ev
) {
3396 ev
= ev
|| {}; // if given no args, make a mock mouse event
3398 if (this.mousedOverSeg
) {
3399 seg
= seg
|| this.mousedOverSeg
; // if given no args, use the currently moused-over segment
3400 this.mousedOverSeg
= null;
3401 this.view
.trigger('eventMouseout', seg
.el
[0], seg
.event
, ev
);
3407 ------------------------------------------------------------------------------------------------------------------*/
3410 // Called when the user does a mousedown on an event, which might lead to dragging.
3411 // Generic enough to work with any type of Grid.
3412 segDragMousedown: function(seg
, ev
) {
3414 var view
= this.view
;
3416 var event
= seg
.event
;
3419 // A clone of the original element that will move with the mouse
3420 var mouseFollower
= new MouseFollower(seg
.el
, {
3422 opacity
: view
.opt('dragOpacity'),
3423 revertDuration
: view
.opt('dragRevertDuration'),
3424 zIndex
: 2 // one above the .fc-view
3427 // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
3429 var dragListener
= new DragListener(view
.coordMap
, {
3431 scroll
: view
.opt('dragScroll'),
3432 listenStart: function(ev
) {
3433 mouseFollower
.hide(); // don't show until we know this is a real drag
3434 mouseFollower
.start(ev
);
3436 dragStart: function(ev
) {
3437 _this
.triggerSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
3438 _this
.isDraggingSeg
= true;
3439 view
.hideEvent(event
); // hide all event segments. our mouseFollower will take over
3440 view
.trigger('eventDragStart', el
[0], event
, ev
, {}); // last argument is jqui dummy
3442 cellOver: function(cell
, isOrig
) {
3443 var origCell
= seg
.cell
|| dragListener
.origCell
; // starting cell could be forced (DayGrid.limit)
3445 dropLocation
= _this
.computeEventDrop(origCell
, cell
, event
);
3447 if (view
.renderDrag(dropLocation
, seg
)) { // have the subclass render a visual indication
3448 mouseFollower
.hide(); // if the subclass is already using a mock event "helper", hide our own
3451 mouseFollower
.show();
3454 dropLocation
= null; // needs to have moved cells to be a valid drop
3458 // have the helper follow the mouse (no snapping) with a warning-style cursor
3459 mouseFollower
.show();
3463 cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
3464 dropLocation
= null;
3465 view
.destroyDrag(); // unrender whatever was done in renderDrag
3466 mouseFollower
.show(); // show in case we are moving out of all cells
3469 dragStop: function(ev
) {
3470 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
3471 mouseFollower
.stop(!dropLocation
, function() {
3472 _this
.isDraggingSeg
= false;
3474 view
.showEvent(event
);
3475 view
.trigger('eventDragStop', el
[0], event
, ev
, {}); // last argument is jqui dummy
3478 view
.reportEventDrop(event
, dropLocation
, el
, ev
);
3483 listenStop: function() {
3484 mouseFollower
.stop(); // put in listenStop in case there was a mousedown but the drag never started
3488 dragListener
.mousedown(ev
); // start listening, which will eventually lead to a dragStart
3492 // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay
3493 // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
3494 // A falsy returned value indicates an invalid drop.
3495 computeEventDrop: function(startCell
, endCell
, event
) {
3496 var dragStart
= startCell
.start
;
3497 var dragEnd
= endCell
.start
;
3504 if (dragStart
.hasTime() === dragEnd
.hasTime()) {
3505 delta
= diffDayTime(dragEnd
, dragStart
);
3506 newStart
= event
.start
.clone().add(delta
);
3507 if (event
.end
=== null) { // do we need to compute an end?
3511 newEnd
= event
.end
.clone().add(delta
);
3513 newAllDay
= event
.allDay
; // keep it the same
3516 // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
3517 newStart
= dragEnd
.clone();
3518 newEnd
= null; // end should be cleared
3519 newAllDay
= !dragEnd
.hasTime();
3528 if (!this.view
.calendar
.isEventRangeAllowed(dropLocation
, event
)) {
3532 return dropLocation
;
3536 /* External Element Dragging
3537 ------------------------------------------------------------------------------------------------------------------*/
3540 // Called when a jQuery UI drag is initiated anywhere in the DOM
3541 documentDragStart: function(ev
, ui
) {
3542 var view
= this.view
;
3546 if (view
.opt('droppable')) { // only listen if this setting is on
3549 // Test that the dragged element passes the dropAccept selector or filter function.
3550 // FYI, the default is "*" (matches all)
3551 accept
= view
.opt('dropAccept');
3552 if ($.isFunction(accept
) ? accept
.call(el
[0], el
) : el
.is(accept
)) {
3554 this.startExternalDrag(el
, ev
, ui
);
3560 // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping
3561 startExternalDrag: function(el
, ev
, ui
) {
3563 var meta
= getDraggedElMeta(el
); // extra data about event drop, including possible event to create
3565 var dropLocation
; // a null value signals an unsuccessful drag
3567 // listener that tracks mouse movement over date-associated pixel regions
3568 dragListener
= new DragListener(this.coordMap
, {
3569 cellOver: function(cell
) {
3570 dropLocation
= _this
.computeExternalDrop(cell
, meta
);
3572 _this
.renderDrag(dropLocation
); // called without a seg parameter
3574 else { // invalid drop cell
3578 cellOut: function() {
3579 dropLocation
= null; // signal unsuccessful
3580 _this
.destroyDrag();
3585 // gets called, only once, when jqui drag is finished
3586 $(document
).one('dragstop', function(ev
, ui
) {
3587 _this
.destroyDrag();
3590 if (dropLocation
) { // element was dropped on a valid date/time cell
3591 _this
.view
.reportExternalDrop(meta
, dropLocation
, el
, ev
, ui
);
3595 dragListener
.startDrag(ev
); // start listening immediately
3599 // Given a cell to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
3600 // returns start/end dates for the event that would result from the hypothetical drop. end might be null.
3601 // Returning a null value signals an invalid drop cell.
3602 computeExternalDrop: function(cell
, meta
) {
3603 var dropLocation
= {
3604 start
: cell
.start
.clone(),
3608 // if dropped on an all-day cell, and element's metadata specified a time, set it
3609 if (meta
.startTime
&& !dropLocation
.start
.hasTime()) {
3610 dropLocation
.start
.time(meta
.startTime
);
3613 if (meta
.duration
) {
3614 dropLocation
.end
= dropLocation
.start
.clone().add(meta
.duration
);
3617 if (!this.view
.calendar
.isExternalDropRangeAllowed(dropLocation
, meta
.eventProps
)) {
3621 return dropLocation
;
3626 /* Drag Rendering (for both events and an external elements)
3627 ------------------------------------------------------------------------------------------------------------------*/
3630 // Renders a visual indication of an event or external element being dragged.
3631 // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
3632 // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
3633 // A truthy returned value indicates this method has rendered a helper element.
3634 renderDrag: function(dropLocation
, seg
) {
3635 // subclasses must implement
3639 // Unrenders a visual indication of an event or external element being dragged
3640 destroyDrag: function() {
3641 // subclasses must implement
3646 ------------------------------------------------------------------------------------------------------------------*/
3649 // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
3650 // Generic enough to work with any type of Grid.
3651 segResizeMousedown: function(seg
, ev
) {
3653 var view
= this.view
;
3654 var calendar
= view
.calendar
;
3656 var event
= seg
.event
;
3657 var start
= event
.start
;
3658 var oldEnd
= calendar
.getEventEnd(event
);
3659 var newEnd
; // falsy if invalid resize
3662 function destroy() { // resets the rendering to show the original event
3663 _this
.destroyEventResize();
3664 view
.showEvent(event
);
3668 // Tracks mouse movement over the *grid's* coordinate map
3669 dragListener
= new DragListener(this.coordMap
, {
3671 scroll
: view
.opt('dragScroll'),
3672 dragStart: function(ev
) {
3673 _this
.triggerSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
3674 _this
.isResizingSeg
= true;
3675 view
.trigger('eventResizeStart', el
[0], event
, ev
, {}); // last argument is jqui dummy
3677 cellOver: function(cell
) {
3680 if (!newEnd
.isAfter(start
)) { // was end moved before start?
3681 newEnd
= start
.clone().add( // make the event span a single slot
3682 diffDayTime(cell
.end
, cell
.start
) // assumes all slot durations are the same
3686 if (newEnd
.isSame(oldEnd
)) {
3689 else if (!calendar
.isEventRangeAllowed({ start
: start
, end
: newEnd
}, event
)) {
3694 _this
.renderEventResize({ start
: start
, end
: newEnd
}, seg
);
3695 view
.hideEvent(event
);
3698 cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
3702 dragStop: function(ev
) {
3703 _this
.isResizingSeg
= false;
3705 view
.trigger('eventResizeStop', el
[0], event
, ev
, {}); // last argument is jqui dummy
3707 if (newEnd
) { // valid date to resize to?
3708 view
.reportEventResize(event
, newEnd
, el
, ev
);
3713 dragListener
.mousedown(ev
); // start listening, which will eventually lead to a dragStart
3717 // Renders a visual indication of an event being resized.
3718 // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
3719 renderEventResize: function(range
, seg
) {
3720 // subclasses must implement
3724 // Unrenders a visual indication of an event being resized.
3725 destroyEventResize: function() {
3726 // subclasses must implement
3731 ------------------------------------------------------------------------------------------------------------------*/
3734 // Compute the text that should be displayed on an event's element.
3735 // `range` can be the Event object itself, or something range-like, with at least a `start`.
3736 // The `timeFormat` options and the grid's default format is used, but `formatStr` can override.
3737 getEventTimeText: function(range
, formatStr
) {
3739 formatStr
= formatStr
|| this.eventTimeFormat
;
3741 if (range
.end
&& this.displayEventEnd
) {
3742 return this.view
.formatRange(range
, formatStr
);
3745 return range
.start
.format(formatStr
);
3750 // Generic utility for generating the HTML classNames for an event segment's element
3751 getSegClasses: function(seg
, isDraggable
, isResizable
) {
3752 var event
= seg
.event
;
3755 seg
.isStart
? 'fc-start' : 'fc-not-start',
3756 seg
.isEnd
? 'fc-end' : 'fc-not-end'
3759 event
.source
? event
.source
.className
: []
3763 classes
.push('fc-draggable');
3766 classes
.push('fc-resizable');
3773 // Utility for generating a CSS string with all the event skin-related properties
3774 getEventSkinCss: function(event
) {
3775 var view
= this.view
;
3776 var source
= event
.source
|| {};
3777 var eventColor
= event
.color
;
3778 var sourceColor
= source
.color
;
3779 var optionColor
= view
.opt('eventColor');
3780 var backgroundColor
=
3781 event
.backgroundColor
||
3783 source
.backgroundColor
||
3785 view
.opt('eventBackgroundColor') ||
3788 event
.borderColor
||
3790 source
.borderColor
||
3792 view
.opt('eventBorderColor') ||
3797 view
.opt('eventTextColor');
3798 var statements
= [];
3799 if (backgroundColor
) {
3800 statements
.push('background-color:' + backgroundColor
);
3803 statements
.push('border-color:' + borderColor
);
3806 statements
.push('color:' + textColor
);
3808 return statements
.join(';');
3812 /* Converting events -> ranges -> segs
3813 ------------------------------------------------------------------------------------------------------------------*/
3816 // Converts an array of event objects into an array of event segment objects.
3817 // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
3818 eventsToSegs: function(events
, rangeToSegsFunc
) {
3819 var eventRanges
= this.eventsToRanges(events
);
3823 for (i
= 0; i
< eventRanges
.length
; i
++) {
3826 this.eventRangeToSegs(eventRanges
[i
], rangeToSegsFunc
)
3834 // Converts an array of events into an array of "range" objects.
3835 // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
3836 // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
3837 // will create an array of ranges that span the time *not* covered by the given event.
3838 eventsToRanges: function(events
) {
3840 var eventsById
= groupEventsById(events
);
3843 // group by ID so that related inverse-background events can be rendered together
3844 $.each(eventsById
, function(id
, eventGroup
) {
3845 if (eventGroup
.length
) {
3848 isInverseBgEvent(eventGroup
[0]) ?
3849 _this
.eventsToInverseRanges(eventGroup
) :
3850 _this
.eventsToNormalRanges(eventGroup
)
3859 // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
3860 eventsToNormalRanges: function(events
) {
3861 var calendar
= this.view
.calendar
;
3864 var eventStart
, eventEnd
;
3866 for (i
= 0; i
< events
.length
; i
++) {
3869 // make copies and normalize by stripping timezone
3870 eventStart
= event
.start
.clone().stripZone();
3871 eventEnd
= calendar
.getEventEnd(event
).stripZone();
3877 eventStartMS
: +eventStart
,
3878 eventDurationMS
: eventEnd
- eventStart
3886 // Converts an array of events, with inverse-background rendering, into an array of range objects.
3887 // The range objects will cover all the time NOT covered by the events.
3888 eventsToInverseRanges: function(events
) {
3889 var view
= this.view
;
3890 var viewStart
= view
.start
.clone().stripZone(); // normalize timezone
3891 var viewEnd
= view
.end
.clone().stripZone(); // normalize timezone
3892 var normalRanges
= this.eventsToNormalRanges(events
); // will give us normalized dates we can use w/o copies
3893 var inverseRanges
= [];
3894 var event0
= events
[0]; // assign this to each range's `.event`
3895 var start
= viewStart
; // the end of the previous range. the start of the new range
3898 // ranges need to be in order. required for our date-walking algorithm
3899 normalRanges
.sort(compareNormalRanges
);
3901 for (i
= 0; i
< normalRanges
.length
; i
++) {
3902 normalRange
= normalRanges
[i
];
3904 // add the span of time before the event (if there is any)
3905 if (normalRange
.start
> start
) { // compare millisecond time (skip any ambig logic)
3906 inverseRanges
.push({
3909 end
: normalRange
.start
3913 start
= normalRange
.end
;
3916 // add the span of time after the last event (if there is any)
3917 if (start
< viewEnd
) { // compare millisecond time (skip any ambig logic)
3918 inverseRanges
.push({
3925 return inverseRanges
;
3929 // Slices the given event range into one or more segment objects.
3930 // A `rangeToSegsFunc` custom slicing function can be given.
3931 eventRangeToSegs: function(eventRange
, rangeToSegsFunc
) {
3935 if (rangeToSegsFunc
) {
3936 segs
= rangeToSegsFunc(eventRange
);
3939 segs
= this.rangeToSegs(eventRange
); // defined by the subclass
3942 for (i
= 0; i
< segs
.length
; i
++) {
3944 seg
.event
= eventRange
.event
;
3945 seg
.eventStartMS
= eventRange
.eventStartMS
;
3946 seg
.eventDurationMS
= eventRange
.eventDurationMS
;
3956 ----------------------------------------------------------------------------------------------------------------------*/
3959 function isBgEvent(event
) { // returns true if background OR inverse-background
3960 var rendering
= getEventRendering(event
);
3961 return rendering
=== 'background' || rendering
=== 'inverse-background';
3965 function isInverseBgEvent(event
) {
3966 return getEventRendering(event
) === 'inverse-background';
3970 function getEventRendering(event
) {
3971 return firstDefined((event
.source
|| {}).rendering
, event
.rendering
);
3975 function groupEventsById(events
) {
3976 var eventsById
= {};
3979 for (i
= 0; i
< events
.length
; i
++) {
3981 (eventsById
[event
._id
] || (eventsById
[event
._id
] = [])).push(event
);
3988 // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
3989 function compareNormalRanges(range1
, range2
) {
3990 return range1
.eventStartMS
- range2
.eventStartMS
; // earlier ranges go first
3994 // A cmp function for determining which segments should take visual priority
3995 // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
3996 function compareSegs(seg1
, seg2
) {
3997 return seg1
.eventStartMS
- seg2
.eventStartMS
|| // earlier events go first
3998 seg2
.eventDurationMS
- seg1
.eventDurationMS
|| // tie? longer events go first
3999 seg2
.event
.allDay
- seg1
.event
.allDay
|| // tie? put all-day events first (booleans cast to 0/1)
4000 (seg1
.event
.title
|| '').localeCompare(seg2
.event
.title
); // tie? alphabetically by title
4003 fc
.compareSegs
= compareSegs
; // export
4006 /* External-Dragging-Element Data
4007 ----------------------------------------------------------------------------------------------------------------------*/
4009 // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
4010 // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
4011 fc
.dataAttrPrefix
= '';
4013 // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
4014 // to be used for Event Object creation.
4015 // A defined `.eventProps`, even when empty, indicates that an event should be created.
4016 function getDraggedElMeta(el
) {
4017 var prefix
= fc
.dataAttrPrefix
;
4018 var eventProps
; // properties for creating the event, not related to date/time
4019 var startTime
; // a Duration
4023 if (prefix
) { prefix
+= '-'; }
4024 eventProps
= el
.data(prefix
+ 'event') || null;
4027 if (typeof eventProps
=== 'object') {
4028 eventProps
= $.extend({}, eventProps
); // make a copy
4030 else { // something like 1 or true. still signal event creation
4034 // pluck special-cased date/time properties
4035 startTime
= eventProps
.start
;
4036 if (startTime
== null) { startTime
= eventProps
.time
; } // accept 'time' as well
4037 duration
= eventProps
.duration
;
4038 stick
= eventProps
.stick
;
4039 delete eventProps
.start
;
4040 delete eventProps
.time
;
4041 delete eventProps
.duration
;
4042 delete eventProps
.stick
;
4045 // fallback to standalone attribute values for each of the date/time properties
4046 if (startTime
== null) { startTime
= el
.data(prefix
+ 'start'); }
4047 if (startTime
== null) { startTime
= el
.data(prefix
+ 'time'); } // accept 'time' as well
4048 if (duration
== null) { duration
= el
.data(prefix
+ 'duration'); }
4049 if (stick
== null) { stick
= el
.data(prefix
+ 'stick'); }
4051 // massage into correct data types
4052 startTime
= startTime
!= null ? moment
.duration(startTime
) : null;
4053 duration
= duration
!= null ? moment
.duration(duration
) : null;
4054 stick
= Boolean(stick
);
4056 return { eventProps
: eventProps
, startTime
: startTime
, duration
: duration
, stick
: stick
};
4060 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
4061 ----------------------------------------------------------------------------------------------------------------------*/
4063 var DayGrid
= Grid
.extend({
4065 numbersVisible
: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
4066 bottomCoordPadding
: 0, // hack for extending the hit area for the last row of the coordinate grid
4067 breakOnWeeks
: null, // should create a new row for each week? set by outside view
4069 cellDates
: null, // flat chronological array of each cell's dates
4070 dayToCellOffsets
: null, // maps days offsets from grid's start date, to cell offsets
4072 rowEls
: null, // set of fake row elements
4073 dayEls
: null, // set of whole-day elements comprising the row's background
4074 helperEls
: null, // set of cell skeleton elements for rendering the mock event "helper"
4077 // Renders the rows and columns into the component's `this.el`, which should already be assigned.
4078 // isRigid determins whether the individual rows should ignore the contents and be a constant height.
4079 // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
4080 render: function(isRigid
) {
4081 var view
= this.view
;
4082 var rowCnt
= this.rowCnt
;
4083 var colCnt
= this.colCnt
;
4084 var cellCnt
= rowCnt
* colCnt
;
4089 for (row
= 0; row
< rowCnt
; row
++) {
4090 html
+= this.dayRowHtml(row
, isRigid
);
4094 this.rowEls
= this.el
.find('.fc-row');
4095 this.dayEls
= this.el
.find('.fc-day');
4097 // trigger dayRender with each cell's element
4098 for (i
= 0; i
< cellCnt
; i
++) {
4099 cell
= this.getCell(i
);
4100 view
.trigger('dayRender', null, cell
.start
, this.dayEls
.eq(i
));
4103 Grid
.prototype.render
.call(this); // call the super-method
4107 destroy: function() {
4108 this.destroySegPopover();
4109 Grid
.prototype.destroy
.call(this); // call the super-method
4113 // Generates the HTML for a single row. `row` is the row number.
4114 dayRowHtml: function(row
, isRigid
) {
4115 var view
= this.view
;
4116 var classes
= [ 'fc-row', 'fc-week', view
.widgetContentClass
];
4119 classes
.push('fc-rigid');
4123 '<div class="' + classes
.join(' ') + '">' +
4124 '<div class="fc-bg">' +
4126 this.rowHtml('day', row
) + // leverages RowRenderer. calls dayCellHtml()
4129 '<div class="fc-content-skeleton">' +
4131 (this.numbersVisible
?
4133 this.rowHtml('number', row
) + // leverages RowRenderer. View will define render method
4143 // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
4144 // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
4145 // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
4146 dayCellHtml: function(cell
) {
4147 return this.bgCellHtml(cell
);
4152 ------------------------------------------------------------------------------------------------------------------*/
4155 // Computes a default column header formatting string if `colFormat` is not explicitly defined
4156 computeColHeadFormat: function() {
4157 if (this.rowCnt
> 1) { // more than one week row. day numbers will be in each cell
4158 return 'ddd'; // "Sat"
4160 else if (this.colCnt
> 1) { // multiple days, so full single date string WON'T be in title text
4161 return this.view
.opt('dayOfMonthFormat'); // "Sat 12/10"
4163 else { // single day, so full single date string will probably be in title text
4164 return 'dddd'; // "Saturday"
4169 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
4170 computeEventTimeFormat: function() {
4171 return this.view
.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
4175 // Computes a default `displayEventEnd` value if one is not expliclty defined
4176 computeDisplayEventEnd: function() {
4177 return this.colCnt
== 1; // we'll likely have space if there's only one day
4182 ------------------------------------------------------------------------------------------------------------------*/
4185 // Initializes row/col information
4186 updateCells: function() {
4192 this.updateCellDates(); // populates cellDates and dayToCellOffsets
4193 cellDates
= this.cellDates
;
4195 if (this.breakOnWeeks
) {
4196 // count columns until the day-of-week repeats
4197 firstDay
= cellDates
[0].day();
4198 for (colCnt
= 1; colCnt
< cellDates
.length
; colCnt
++) {
4199 if (cellDates
[colCnt
].day() == firstDay
) {
4203 rowCnt
= Math
.ceil(cellDates
.length
/ colCnt
);
4207 colCnt
= cellDates
.length
;
4210 this.rowCnt
= rowCnt
;
4211 this.colCnt
= colCnt
;
4215 // Populates cellDates and dayToCellOffsets
4216 updateCellDates: function() {
4217 var view
= this.view
;
4218 var date
= this.start
.clone();
4223 while (date
.isBefore(this.end
)) { // loop each day from start to end
4224 if (view
.isHiddenDay(date
)) {
4225 offsets
.push(offset
+ 0.5); // mark that it's between offsets
4229 offsets
.push(offset
);
4230 dates
.push(date
.clone());
4232 date
.add(1, 'days');
4235 this.cellDates
= dates
;
4236 this.dayToCellOffsets
= offsets
;
4240 // Given a cell object, generates a range object
4241 computeCellRange: function(cell
) {
4242 var colCnt
= this.colCnt
;
4243 var index
= cell
.row
* colCnt
+ (this.isRTL
? colCnt
- cell
.col
- 1 : cell
.col
);
4244 var start
= this.cellDates
[index
].clone();
4245 var end
= start
.clone().add(1, 'day');
4247 return { start
: start
, end
: end
};
4251 // Retrieves the element representing the given row
4252 getRowEl: function(row
) {
4253 return this.rowEls
.eq(row
);
4257 // Retrieves the element representing the given column
4258 getColEl: function(col
) {
4259 return this.dayEls
.eq(col
);
4263 // Gets the whole-day element associated with the cell
4264 getCellDayEl: function(cell
) {
4265 return this.dayEls
.eq(cell
.row
* this.colCnt
+ cell
.col
);
4269 // Overrides Grid's method for when row coordinates are computed
4270 computeRowCoords: function() {
4271 var rowCoords
= Grid
.prototype.computeRowCoords
.call(this); // call the super-method
4273 // hack for extending last row (used by AgendaView)
4274 rowCoords
[rowCoords
.length
- 1].bottom
+= this.bottomCoordPadding
;
4281 ------------------------------------------------------------------------------------------------------------------*/
4284 // Slices up a date range by row into an array of segments
4285 rangeToSegs: function(range
) {
4286 var isRTL
= this.isRTL
;
4287 var rowCnt
= this.rowCnt
;
4288 var colCnt
= this.colCnt
;
4290 var first
, last
; // inclusive cell-offset range for given range
4292 var rowFirst
, rowLast
; // inclusive cell-offset range for current row
4294 var segFirst
, segLast
; // inclusive cell-offset range for segment
4297 range
= this.view
.computeDayRange(range
); // make whole-day range, considering nextDayThreshold
4298 first
= this.dateToCellOffset(range
.start
);
4299 last
= this.dateToCellOffset(range
.end
.subtract(1, 'days')); // offset of inclusive end date
4301 for (row
= 0; row
< rowCnt
; row
++) {
4302 rowFirst
= row
* colCnt
;
4303 rowLast
= rowFirst
+ colCnt
- 1;
4305 // intersect segment's offset range with the row's
4306 segFirst
= Math
.max(rowFirst
, first
);
4307 segLast
= Math
.min(rowLast
, last
);
4309 // deal with in-between indices
4310 segFirst
= Math
.ceil(segFirst
); // in-between starts round to next cell
4311 segLast
= Math
.floor(segLast
); // in-between ends round to prev cell
4313 if (segFirst
<= segLast
) { // was there any intersection with the current row?
4315 // must be matching integers to be the segment's start/end
4316 isStart
= segFirst
=== first
;
4317 isEnd
= segLast
=== last
;
4319 // translate offsets to be relative to start-of-row
4320 segFirst
-= rowFirst
;
4321 segLast
-= rowFirst
;
4323 seg
= { row
: row
, isStart
: isStart
, isEnd
: isEnd
};
4325 seg
.leftCol
= colCnt
- segLast
- 1;
4326 seg
.rightCol
= colCnt
- segFirst
- 1;
4329 seg
.leftCol
= segFirst
;
4330 seg
.rightCol
= segLast
;
4340 // Given a date, returns its chronolocial cell-offset from the first cell of the grid.
4341 // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
4342 // If before the first offset, returns a negative number.
4343 // If after the last offset, returns an offset past the last cell offset.
4344 // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
4345 dateToCellOffset: function(date
) {
4346 var offsets
= this.dayToCellOffsets
;
4347 var day
= date
.diff(this.start
, 'days');
4350 return offsets
[0] - 1;
4352 else if (day
>= offsets
.length
) {
4353 return offsets
[offsets
.length
- 1] + 1;
4356 return offsets
[day
];
4361 /* Event Drag Visualization
4362 ------------------------------------------------------------------------------------------------------------------*/
4363 // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
4366 // Renders a visual indication of an event or external element being dragged.
4367 // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info.
4368 renderDrag: function(dropLocation
, seg
) {
4371 // always render a highlight underneath
4372 this.renderHighlight(
4373 this.view
.calendar
.ensureVisibleEventRange(dropLocation
) // needs to be a proper range
4376 // if a segment from the same calendar but another component is being dragged, render a helper event
4377 if (seg
&& !seg
.el
.closest(this.el
).length
) {
4379 this.renderRangeHelper(dropLocation
, seg
);
4381 opacity
= this.view
.opt('dragOpacity');
4382 if (opacity
!== undefined) {
4383 this.helperEls
.css('opacity', opacity
);
4386 return true; // a helper has been rendered
4391 // Unrenders any visual indication of a hovering event
4392 destroyDrag: function() {
4393 this.destroyHighlight();
4394 this.destroyHelper();
4398 /* Event Resize Visualization
4399 ------------------------------------------------------------------------------------------------------------------*/
4402 // Renders a visual indication of an event being resized
4403 renderEventResize: function(range
, seg
) {
4404 this.renderHighlight(range
);
4405 this.renderRangeHelper(range
, seg
);
4409 // Unrenders a visual indication of an event being resized
4410 destroyEventResize: function() {
4411 this.destroyHighlight();
4412 this.destroyHelper();
4417 ------------------------------------------------------------------------------------------------------------------*/
4420 // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
4421 renderHelper: function(event
, sourceSeg
) {
4422 var helperNodes
= [];
4423 var segs
= this.eventsToSegs([ event
]);
4426 segs
= this.renderFgSegEls(segs
); // assigns each seg's el and returns a subset of segs that were rendered
4427 rowStructs
= this.renderSegRows(segs
);
4429 // inject each new event skeleton into each associated row
4430 this.rowEls
.each(function(row
, rowNode
) {
4431 var rowEl
= $(rowNode
); // the .fc-row
4432 var skeletonEl
= $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
4435 // If there is an original segment, match the top position. Otherwise, put it at the row's top level
4436 if (sourceSeg
&& sourceSeg
.row
=== row
) {
4437 skeletonTop
= sourceSeg
.el
.position().top
;
4440 skeletonTop
= rowEl
.find('.fc-content-skeleton tbody').position().top
;
4443 skeletonEl
.css('top', skeletonTop
)
4445 .append(rowStructs
[row
].tbodyEl
);
4447 rowEl
.append(skeletonEl
);
4448 helperNodes
.push(skeletonEl
[0]);
4451 this.helperEls
= $(helperNodes
); // array -> jQuery set
4455 // Unrenders any visual indication of a mock helper event
4456 destroyHelper: function() {
4457 if (this.helperEls
) {
4458 this.helperEls
.remove();
4459 this.helperEls
= null;
4464 /* Fill System (highlight, background events, business hours)
4465 ------------------------------------------------------------------------------------------------------------------*/
4468 fillSegTag
: 'td', // override the default tag name
4471 // Renders a set of rectangles over the given segments of days.
4472 // Only returns segments that successfully rendered.
4473 renderFill: function(type
, segs
) {
4478 segs
= this.renderFillSegEls(type
, segs
); // assignes `.el` to each seg. returns successfully rendered segs
4480 for (i
= 0; i
< segs
.length
; i
++) {
4482 skeletonEl
= this.renderFillRow(type
, seg
);
4483 this.rowEls
.eq(seg
.row
).append(skeletonEl
);
4484 nodes
.push(skeletonEl
[0]);
4487 this.elsByFill
[type
] = $(nodes
);
4493 // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
4494 renderFillRow: function(type
, seg
) {
4495 var colCnt
= this.colCnt
;
4496 var startCol
= seg
.leftCol
;
4497 var endCol
= seg
.rightCol
+ 1;
4502 '<div class="fc-' + type
.toLowerCase() + '-skeleton">' +
4503 '<table><tr/></table>' +
4506 trEl
= skeletonEl
.find('tr');
4509 trEl
.append('<td colspan="' + startCol
+ '"/>');
4513 seg
.el
.attr('colspan', endCol
- startCol
)
4516 if (endCol
< colCnt
) {
4517 trEl
.append('<td colspan="' + (colCnt
- endCol
) + '"/>');
4520 this.bookendCells(trEl
, type
);
4527 /* Event-rendering methods for the DayGrid class
4528 ----------------------------------------------------------------------------------------------------------------------*/
4532 rowStructs
: null, // an array of objects, each holding information about a row's foreground event-rendering
4535 // Unrenders all events currently rendered on the grid
4536 destroyEvents: function() {
4537 this.destroySegPopover(); // removes the "more.." events popover
4538 Grid
.prototype.destroyEvents
.apply(this, arguments
); // calls the super-method
4542 // Retrieves all rendered segment objects currently rendered on the grid
4543 getEventSegs: function() {
4544 return Grid
.prototype.getEventSegs
.call(this) // get the segments from the super-method
4545 .concat(this.popoverSegs
|| []); // append the segments from the "more..." popover
4549 // Renders the given background event segments onto the grid
4550 renderBgSegs: function(segs
) {
4552 // don't render timed background events
4553 var allDaySegs
= $.grep(segs
, function(seg
) {
4554 return seg
.event
.allDay
;
4557 return Grid
.prototype.renderBgSegs
.call(this, allDaySegs
); // call the super-method
4561 // Renders the given foreground event segments onto the grid
4562 renderFgSegs: function(segs
) {
4565 // render an `.el` on each seg
4566 // returns a subset of the segs. segs that were actually rendered
4567 segs
= this.renderFgSegEls(segs
);
4569 rowStructs
= this.rowStructs
= this.renderSegRows(segs
);
4571 // append to each row's content skeleton
4572 this.rowEls
.each(function(i
, rowNode
) {
4573 $(rowNode
).find('.fc-content-skeleton > table').append(
4574 rowStructs
[i
].tbodyEl
4578 return segs
; // return only the segs that were actually rendered
4582 // Unrenders all currently rendered foreground event segments
4583 destroyFgSegs: function() {
4584 var rowStructs
= this.rowStructs
|| [];
4587 while ((rowStruct
= rowStructs
.pop())) {
4588 rowStruct
.tbodyEl
.remove();
4591 this.rowStructs
= null;
4595 // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
4596 // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
4597 // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
4598 renderSegRows: function(segs
) {
4599 var rowStructs
= [];
4603 segRows
= this.groupSegRows(segs
); // group into nested arrays
4605 // iterate each row of segment groupings
4606 for (row
= 0; row
< segRows
.length
; row
++) {
4608 this.renderSegRow(row
, segRows
[row
])
4616 // Builds the HTML to be used for the default element for an individual segment
4617 fgSegHtml: function(seg
, disableResizing
) {
4618 var view
= this.view
;
4619 var event
= seg
.event
;
4620 var isDraggable
= view
.isEventDraggable(event
);
4621 var isResizable
= !disableResizing
&& event
.allDay
&& seg
.isEnd
&& view
.isEventResizable(event
);
4622 var classes
= this.getSegClasses(seg
, isDraggable
, isResizable
);
4623 var skinCss
= this.getEventSkinCss(event
);
4627 classes
.unshift('fc-day-grid-event');
4629 // Only display a timed events time if it is the starting segment
4630 if (!event
.allDay
&& seg
.isStart
) {
4631 timeHtml
= '<span class="fc-time">' + htmlEscape(this.getEventTimeText(event
)) + '</span>';
4635 '<span class="fc-title">' +
4636 (htmlEscape(event
.title
|| '') || ' ') + // we always want one line of height
4639 return '<a class="' + classes
.join(' ') + '"' +
4641 ' href="' + htmlEscape(event
.url
) + '"' :
4645 ' style="' + skinCss
+ '"' :
4649 '<div class="fc-content">' +
4651 titleHtml
+ ' ' + timeHtml
: // put a natural space in between
4652 timeHtml
+ ' ' + titleHtml
//
4656 '<div class="fc-resizer"/>' :
4663 // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
4664 // the segments. Returns object with a bunch of internal data about how the render was calculated.
4665 renderSegRow: function(row
, rowSegs
) {
4666 var colCnt
= this.colCnt
;
4667 var segLevels
= this.buildSegLevels(rowSegs
); // group into sub-arrays of levels
4668 var levelCnt
= Math
.max(1, segLevels
.length
); // ensure at least one level
4669 var tbody
= $('<tbody/>');
4670 var segMatrix
= []; // lookup for which segments are rendered into which level+col cells
4671 var cellMatrix
= []; // lookup for all <td> elements of the level+col matrix
4672 var loneCellMatrix
= []; // lookup for <td> elements that only take up a single column
4679 // populates empty cells from the current column (`col`) to `endCol`
4680 function emptyCellsUntil(endCol
) {
4681 while (col
< endCol
) {
4682 // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
4683 td
= (loneCellMatrix
[i
- 1] || [])[col
];
4687 parseInt(td
.attr('rowspan') || 1, 10) + 1
4694 cellMatrix
[i
][col
] = td
;
4695 loneCellMatrix
[i
][col
] = td
;
4700 for (i
= 0; i
< levelCnt
; i
++) { // iterate through all levels
4701 levelSegs
= segLevels
[i
];
4706 cellMatrix
.push([]);
4707 loneCellMatrix
.push([]);
4709 // levelCnt might be 1 even though there are no actual levels. protect against this.
4710 // this single empty row is useful for styling.
4712 for (j
= 0; j
< levelSegs
.length
; j
++) { // iterate through segments in level
4715 emptyCellsUntil(seg
.leftCol
);
4717 // create a container that occupies or more columns. append the event element.
4718 td
= $('<td class="fc-event-container"/>').append(seg
.el
);
4719 if (seg
.leftCol
!= seg
.rightCol
) {
4720 td
.attr('colspan', seg
.rightCol
- seg
.leftCol
+ 1);
4722 else { // a single-column segment
4723 loneCellMatrix
[i
][col
] = td
;
4726 while (col
<= seg
.rightCol
) {
4727 cellMatrix
[i
][col
] = td
;
4728 segMatrix
[i
][col
] = seg
;
4736 emptyCellsUntil(colCnt
); // finish off the row
4737 this.bookendCells(tr
, 'eventSkeleton');
4741 return { // a "rowStruct"
4742 row
: row
, // the row number
4744 cellMatrix
: cellMatrix
,
4745 segMatrix
: segMatrix
,
4746 segLevels
: segLevels
,
4752 // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
4753 buildSegLevels: function(segs
) {
4758 // Give preference to elements with certain criteria, so they have
4759 // a chance to be closer to the top.
4760 segs
.sort(compareSegs
);
4762 for (i
= 0; i
< segs
.length
; i
++) {
4765 // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
4766 for (j
= 0; j
< levels
.length
; j
++) {
4767 if (!isDaySegCollision(seg
, levels
[j
])) {
4771 // `j` now holds the desired subrow index
4774 // create new level array if needed and append segment
4775 (levels
[j
] || (levels
[j
] = [])).push(seg
);
4778 // order segments left-to-right. very important if calendar is RTL
4779 for (j
= 0; j
< levels
.length
; j
++) {
4780 levels
[j
].sort(compareDaySegCols
);
4787 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
4788 groupSegRows: function(segs
) {
4792 for (i
= 0; i
< this.rowCnt
; i
++) {
4796 for (i
= 0; i
< segs
.length
; i
++) {
4797 segRows
[segs
[i
].row
].push(segs
[i
]);
4806 // Computes whether two segments' columns collide. They are assumed to be in the same row.
4807 function isDaySegCollision(seg
, otherSegs
) {
4810 for (i
= 0; i
< otherSegs
.length
; i
++) {
4811 otherSeg
= otherSegs
[i
];
4814 otherSeg
.leftCol
<= seg
.rightCol
&&
4815 otherSeg
.rightCol
>= seg
.leftCol
4825 // A cmp function for determining the leftmost event
4826 function compareDaySegCols(a
, b
) {
4827 return a
.leftCol
- b
.leftCol
;
4830 /* Methods relate to limiting the number events for a given day on a DayGrid
4831 ----------------------------------------------------------------------------------------------------------------------*/
4832 // NOTE: all the segs being passed around in here are foreground segs
4836 segPopover
: null, // the Popover that holds events that can't fit in a cell. null when not visible
4837 popoverSegs
: null, // an array of segment objects that the segPopover holds. null when not visible
4840 destroySegPopover: function() {
4841 if (this.segPopover
) {
4842 this.segPopover
.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
4847 // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
4848 // `levelLimit` can be false (don't limit), a number, or true (should be computed).
4849 limitRows: function(levelLimit
) {
4850 var rowStructs
= this.rowStructs
|| [];
4854 for (row
= 0; row
< rowStructs
.length
; row
++) {
4855 this.unlimitRow(row
);
4858 rowLevelLimit
= false;
4860 else if (typeof levelLimit
=== 'number') {
4861 rowLevelLimit
= levelLimit
;
4864 rowLevelLimit
= this.computeRowLevelLimit(row
);
4867 if (rowLevelLimit
!== false) {
4868 this.limitRow(row
, rowLevelLimit
);
4874 // Computes the number of levels a row will accomodate without going outside its bounds.
4875 // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
4876 // `row` is the row number.
4877 computeRowLevelLimit: function(row
) {
4878 var rowEl
= this.rowEls
.eq(row
); // the containing "fake" row div
4879 var rowHeight
= rowEl
.height(); // TODO: cache somehow?
4880 var trEls
= this.rowStructs
[row
].tbodyEl
.children();
4883 // Reveal one level <tr> at a time and stop when we find one out of bounds
4884 for (i
= 0; i
< trEls
.length
; i
++) {
4885 trEl
= trEls
.eq(i
).removeClass('fc-limited'); // get and reveal
4886 if (trEl
.position().top
+ trEl
.outerHeight() > rowHeight
) {
4891 return false; // should not limit at all
4895 // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
4896 // `row` is the row number.
4897 // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
4898 limitRow: function(row
, levelLimit
) {
4900 var rowStruct
= this.rowStructs
[row
];
4901 var moreNodes
= []; // array of "more" <a> links and <td> DOM nodes
4902 var col
= 0; // col #, left-to-right (not chronologically)
4904 var levelSegs
; // array of segment objects in the last allowable level, ordered left-to-right
4905 var cellMatrix
; // a matrix (by level, then column) of all <td> jQuery elements in the row
4906 var limitedNodes
; // array of temporarily hidden level <tr> and segment <td> DOM nodes
4908 var segsBelow
; // array of segment objects below `seg` in the current `col`
4909 var totalSegsBelow
; // total number of segments below `seg` in any of the columns `seg` occupies
4910 var colSegsBelow
; // array of segment arrays, below seg, one for each column (offset from segs's first column)
4912 var segMoreNodes
; // array of "more" <td> cells that will stand-in for the current seg's cell
4914 var moreTd
, moreWrap
, moreLink
;
4916 // Iterates through empty level cells and places "more" links inside if need be
4917 function emptyCellsUntil(endCol
) { // goes from current `col` to `endCol`
4918 while (col
< endCol
) {
4919 cell
= _this
.getCell(row
, col
);
4920 segsBelow
= _this
.getCellSegs(cell
, levelLimit
);
4921 if (segsBelow
.length
) {
4922 td
= cellMatrix
[levelLimit
- 1][col
];
4923 moreLink
= _this
.renderMoreLink(cell
, segsBelow
);
4924 moreWrap
= $('<div/>').append(moreLink
);
4925 td
.append(moreWrap
);
4926 moreNodes
.push(moreWrap
[0]);
4932 if (levelLimit
&& levelLimit
< rowStruct
.segLevels
.length
) { // is it actually over the limit?
4933 levelSegs
= rowStruct
.segLevels
[levelLimit
- 1];
4934 cellMatrix
= rowStruct
.cellMatrix
;
4936 limitedNodes
= rowStruct
.tbodyEl
.children().slice(levelLimit
) // get level <tr> elements past the limit
4937 .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
4939 // iterate though segments in the last allowable level
4940 for (i
= 0; i
< levelSegs
.length
; i
++) {
4942 emptyCellsUntil(seg
.leftCol
); // process empty cells before the segment
4944 // determine *all* segments below `seg` that occupy the same columns
4947 while (col
<= seg
.rightCol
) {
4948 cell
= this.getCell(row
, col
);
4949 segsBelow
= this.getCellSegs(cell
, levelLimit
);
4950 colSegsBelow
.push(segsBelow
);
4951 totalSegsBelow
+= segsBelow
.length
;
4955 if (totalSegsBelow
) { // do we need to replace this segment with one or many "more" links?
4956 td
= cellMatrix
[levelLimit
- 1][seg
.leftCol
]; // the segment's parent cell
4957 rowspan
= td
.attr('rowspan') || 1;
4960 // make a replacement <td> for each column the segment occupies. will be one for each colspan
4961 for (j
= 0; j
< colSegsBelow
.length
; j
++) {
4962 moreTd
= $('<td class="fc-more-cell"/>').attr('rowspan', rowspan
);
4963 segsBelow
= colSegsBelow
[j
];
4964 cell
= this.getCell(row
, seg
.leftCol
+ j
);
4965 moreLink
= this.renderMoreLink(cell
, [ seg
].concat(segsBelow
)); // count seg as hidden too
4966 moreWrap
= $('<div/>').append(moreLink
);
4967 moreTd
.append(moreWrap
);
4968 segMoreNodes
.push(moreTd
[0]);
4969 moreNodes
.push(moreTd
[0]);
4972 td
.addClass('fc-limited').after($(segMoreNodes
)); // hide original <td> and inject replacements
4973 limitedNodes
.push(td
[0]);
4977 emptyCellsUntil(this.colCnt
); // finish off the level
4978 rowStruct
.moreEls
= $(moreNodes
); // for easy undoing later
4979 rowStruct
.limitedEls
= $(limitedNodes
); // for easy undoing later
4984 // Reveals all levels and removes all "more"-related elements for a grid's row.
4985 // `row` is a row number.
4986 unlimitRow: function(row
) {
4987 var rowStruct
= this.rowStructs
[row
];
4989 if (rowStruct
.moreEls
) {
4990 rowStruct
.moreEls
.remove();
4991 rowStruct
.moreEls
= null;
4994 if (rowStruct
.limitedEls
) {
4995 rowStruct
.limitedEls
.removeClass('fc-limited');
4996 rowStruct
.limitedEls
= null;
5001 // Renders an <a> element that represents hidden event element for a cell.
5002 // Responsible for attaching click handler as well.
5003 renderMoreLink: function(cell
, hiddenSegs
) {
5005 var view
= this.view
;
5007 return $('<a class="fc-more"/>')
5009 this.getMoreLinkText(hiddenSegs
.length
)
5011 .on('click', function(ev
) {
5012 var clickOption
= view
.opt('eventLimitClick');
5013 var date
= cell
.start
;
5014 var moreEl
= $(this);
5015 var dayEl
= _this
.getCellDayEl(cell
);
5016 var allSegs
= _this
.getCellSegs(cell
);
5018 // rescope the segments to be within the cell's date
5019 var reslicedAllSegs
= _this
.resliceDaySegs(allSegs
, date
);
5020 var reslicedHiddenSegs
= _this
.resliceDaySegs(hiddenSegs
, date
);
5022 if (typeof clickOption
=== 'function') {
5023 // the returned value can be an atomic option
5024 clickOption
= view
.trigger('eventLimitClick', null, {
5028 segs
: reslicedAllSegs
,
5029 hiddenSegs
: reslicedHiddenSegs
5033 if (clickOption
=== 'popover') {
5034 _this
.showSegPopover(cell
, moreEl
, reslicedAllSegs
);
5036 else if (typeof clickOption
=== 'string') { // a view name
5037 view
.calendar
.zoomTo(date
, clickOption
);
5043 // Reveals the popover that displays all events within a cell
5044 showSegPopover: function(cell
, moreLink
, segs
) {
5046 var view
= this.view
;
5047 var moreWrap
= moreLink
.parent(); // the <div> wrapper around the <a>
5048 var topEl
; // the element we want to match the top coordinate of
5051 if (this.rowCnt
== 1) {
5052 topEl
= view
.el
; // will cause the popover to cover any sort of header
5055 topEl
= this.rowEls
.eq(cell
.row
); // will align with top of row
5059 className
: 'fc-more-popover',
5060 content
: this.renderSegPopoverContent(cell
, segs
),
5062 top
: topEl
.offset().top
,
5063 autoHide
: true, // when the user clicks elsewhere, hide the popover
5064 viewportConstrain
: view
.opt('popoverViewportConstrain'),
5066 // destroy everything when the popover is hidden
5067 _this
.segPopover
.destroy();
5068 _this
.segPopover
= null;
5069 _this
.popoverSegs
= null;
5073 // Determine horizontal coordinate.
5074 // We use the moreWrap instead of the <td> to avoid border confusion.
5076 options
.right
= moreWrap
.offset().left
+ moreWrap
.outerWidth() + 1; // +1 to be over cell border
5079 options
.left
= moreWrap
.offset().left
- 1; // -1 to be over cell border
5082 this.segPopover
= new Popover(options
);
5083 this.segPopover
.show();
5087 // Builds the inner DOM contents of the segment popover
5088 renderSegPopoverContent: function(cell
, segs
) {
5089 var view
= this.view
;
5090 var isTheme
= view
.opt('theme');
5091 var title
= cell
.start
.format(view
.opt('dayPopoverFormat'));
5093 '<div class="fc-header ' + view
.widgetHeaderClass
+ '">' +
5094 '<span class="fc-close ' +
5095 (isTheme
? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
5097 '<span class="fc-title">' +
5100 '<div class="fc-clear"/>' +
5102 '<div class="fc-body ' + view
.widgetContentClass
+ '">' +
5103 '<div class="fc-event-container"></div>' +
5106 var segContainer
= content
.find('.fc-event-container');
5109 // render each seg's `el` and only return the visible segs
5110 segs
= this.renderFgSegEls(segs
, true); // disableResizing=true
5111 this.popoverSegs
= segs
;
5113 for (i
= 0; i
< segs
.length
; i
++) {
5115 // because segments in the popover are not part of a grid coordinate system, provide a hint to any
5116 // grids that want to do drag-n-drop about which cell it came from
5117 segs
[i
].cell
= cell
;
5119 segContainer
.append(segs
[i
].el
);
5126 // Given the events within an array of segment objects, reslice them to be in a single day
5127 resliceDaySegs: function(segs
, dayDate
) {
5129 // build an array of the original events
5130 var events
= $.map(segs
, function(seg
) {
5134 var dayStart
= dayDate
.clone().stripTime();
5135 var dayEnd
= dayStart
.clone().add(1, 'days');
5136 var dayRange
= { start
: dayStart
, end
: dayEnd
};
5138 // slice the events with a custom slicing function
5139 return this.eventsToSegs(
5142 var seg
= intersectionToSeg(range
, dayRange
); // undefind if no intersection
5143 return seg
? [ seg
] : []; // must return an array of segments
5149 // Generates the text that should be inside a "more" link, given the number of events it represents
5150 getMoreLinkText: function(num
) {
5151 var opt
= this.view
.opt('eventLimitText');
5153 if (typeof opt
=== 'function') {
5157 return '+' + num
+ ' ' + opt
;
5162 // Returns segments within a given cell.
5163 // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
5164 getCellSegs: function(cell
, startLevel
) {
5165 var segMatrix
= this.rowStructs
[cell
.row
].segMatrix
;
5166 var level
= startLevel
|| 0;
5170 while (level
< segMatrix
.length
) {
5171 seg
= segMatrix
[level
][cell
.col
];
5183 /* A component that renders one or more columns of vertical time slots
5184 ----------------------------------------------------------------------------------------------------------------------*/
5186 var TimeGrid
= Grid
.extend({
5188 slotDuration
: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
5189 snapDuration
: null, // granularity of time for dragging and selecting
5191 minTime
: null, // Duration object that denotes the first visible time of any given day
5192 maxTime
: null, // Duration object that denotes the exclusive visible end time of any given day
5194 axisFormat
: null, // formatting string for times running along vertical axis
5196 dayEls
: null, // cells elements in the day-row background
5197 slatEls
: null, // elements running horizontally across all columns
5199 slatTops
: null, // an array of top positions, relative to the container. last item holds bottom of last slot
5201 helperEl
: null, // cell skeleton element for rendering the mock event "helper"
5203 businessHourSegs
: null,
5206 constructor: function() {
5207 Grid
.apply(this, arguments
); // call the super-constructor
5208 this.processOptions();
5212 // Renders the time grid into `this.el`, which should already be assigned.
5213 // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
5214 render: function() {
5215 this.el
.html(this.renderHtml());
5216 this.dayEls
= this.el
.find('.fc-day');
5217 this.slatEls
= this.el
.find('.fc-slats tr');
5219 this.computeSlatTops();
5220 this.renderBusinessHours();
5221 Grid
.prototype.render
.call(this); // call the super-method
5225 renderBusinessHours: function() {
5226 var events
= this.view
.calendar
.getBusinessHoursEvents();
5227 this.businessHourSegs
= this.renderFill('businessHours', this.eventsToSegs(events
), 'bgevent');
5231 // Renders the basic HTML skeleton for the grid
5232 renderHtml: function() {
5234 '<div class="fc-bg">' +
5236 this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
5239 '<div class="fc-slats">' +
5241 this.slatRowHtml() +
5247 // Renders the HTML for a vertical background cell behind the slots.
5248 // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
5249 slotBgCellHtml: function(cell
) {
5250 return this.bgCellHtml(cell
);
5254 // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
5255 slatRowHtml: function() {
5256 var view
= this.view
;
5257 var isRTL
= this.isRTL
;
5259 var slotNormal
= this.slotDuration
.asMinutes() % 15 === 0;
5260 var slotTime
= moment
.duration(+this.minTime
); // wish there was .clone() for durations
5261 var slotDate
; // will be on the view's first day, but we only care about its time
5265 // Calculate the time for each slot
5266 while (slotTime
< this.maxTime
) {
5267 slotDate
= this.start
.clone().time(slotTime
); // will be in UTC but that's good. to avoid DST issues
5268 minutes
= slotDate
.minutes();
5271 '<td class="fc-axis fc-time ' + view
.widgetContentClass
+ '" ' + view
.axisStyleAttr() + '>' +
5272 ((!slotNormal
|| !minutes
) ? // if irregular slot duration, or on the hour, then display the time
5273 '<span>' + // for matchCellWidths
5274 htmlEscape(slotDate
.format(this.axisFormat
)) +
5281 '<tr ' + (!minutes
? '' : 'class="fc-minor"') + '>' +
5282 (!isRTL
? axisHtml
: '') +
5283 '<td class="' + view
.widgetContentClass
+ '"/>' +
5284 (isRTL
? axisHtml
: '') +
5287 slotTime
.add(this.slotDuration
);
5295 ------------------------------------------------------------------------------------------------------------------*/
5298 // Parses various options into properties of this object
5299 processOptions: function() {
5300 var view
= this.view
;
5301 var slotDuration
= view
.opt('slotDuration');
5302 var snapDuration
= view
.opt('snapDuration');
5304 slotDuration
= moment
.duration(slotDuration
);
5305 snapDuration
= snapDuration
? moment
.duration(snapDuration
) : slotDuration
;
5307 this.slotDuration
= slotDuration
;
5308 this.snapDuration
= snapDuration
;
5310 this.minTime
= moment
.duration(view
.opt('minTime'));
5311 this.maxTime
= moment
.duration(view
.opt('maxTime'));
5313 this.axisFormat
= view
.opt('axisFormat') || view
.opt('smallTimeFormat');
5317 // Computes a default column header formatting string if `colFormat` is not explicitly defined
5318 computeColHeadFormat: function() {
5319 if (this.colCnt
> 1) { // multiple days, so full single date string WON'T be in title text
5320 return this.view
.opt('dayOfMonthFormat'); // "Sat 12/10"
5322 else { // single day, so full single date string will probably be in title text
5323 return 'dddd'; // "Saturday"
5328 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
5329 computeEventTimeFormat: function() {
5330 return this.view
.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
5334 // Computes a default `displayEventEnd` value if one is not expliclty defined
5335 computeDisplayEventEnd: function() {
5341 ------------------------------------------------------------------------------------------------------------------*/
5344 // Initializes row/col information
5345 updateCells: function() {
5346 var view
= this.view
;
5350 date
= this.start
.clone();
5351 while (date
.isBefore(this.end
)) {
5356 date
= view
.skipHiddenDays(date
);
5363 this.colData
= colData
;
5364 this.colCnt
= colData
.length
;
5365 this.rowCnt
= Math
.ceil((this.maxTime
- this.minTime
) / this.snapDuration
); // # of vertical snaps
5369 // Given a cell object, generates a range object
5370 computeCellRange: function(cell
) {
5371 var time
= this.computeSnapTime(cell
.row
);
5372 var start
= this.view
.calendar
.rezoneDate(cell
.day
).time(time
);
5373 var end
= start
.clone().add(this.snapDuration
);
5375 return { start
: start
, end
: end
};
5379 // Retrieves the element representing the given column
5380 getColEl: function(col
) {
5381 return this.dayEls
.eq(col
);
5386 ------------------------------------------------------------------------------------------------------------------*/
5389 // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
5390 computeSnapTime: function(row
) {
5391 return moment
.duration(this.minTime
+ this.snapDuration
* row
);
5395 // Slices up a date range by column into an array of segments
5396 rangeToSegs: function(range
) {
5397 var colCnt
= this.colCnt
;
5406 start
: range
.start
.clone().stripZone(),
5407 end
: range
.end
.clone().stripZone()
5410 for (col
= 0; col
< colCnt
; col
++) {
5411 colDate
= this.colData
[col
].day
; // will be ambig time/timezone
5413 start
: colDate
.clone().time(this.minTime
),
5414 end
: colDate
.clone().time(this.maxTime
)
5416 seg
= intersectionToSeg(range
, colRange
); // both will be ambig timezone
5428 ------------------------------------------------------------------------------------------------------------------*/
5431 // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
5432 resize: function() {
5433 this.computeSlatTops();
5434 this.updateSegVerticals();
5438 // Computes the top/bottom coordinates of each "snap" rows
5439 computeRowCoords: function() {
5440 var originTop
= this.el
.offset().top
;
5445 for (i
= 0; i
< this.rowCnt
; i
++) {
5447 top
: originTop
+ this.computeTimeTop(this.computeSnapTime(i
))
5450 items
[i
- 1].bottom
= item
.top
;
5454 item
.bottom
= item
.top
+ this.computeTimeTop(this.computeSnapTime(i
));
5460 // Computes the top coordinate, relative to the bounds of the grid, of the given date.
5461 // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
5462 computeDateTop: function(date
, startOfDayDate
) {
5463 return this.computeTimeTop(
5465 date
.clone().stripZone() - startOfDayDate
.clone().stripTime()
5471 // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
5472 computeTimeTop: function(time
) {
5473 var slatCoverage
= (time
- this.minTime
) / this.slotDuration
; // floating-point value of # of slots covered
5479 // constrain. because minTime/maxTime might be customized
5480 slatCoverage
= Math
.max(0, slatCoverage
);
5481 slatCoverage
= Math
.min(this.slatEls
.length
, slatCoverage
);
5483 slatIndex
= Math
.floor(slatCoverage
); // an integer index of the furthest whole slot
5484 slatRemainder
= slatCoverage
- slatIndex
;
5485 slatTop
= this.slatTops
[slatIndex
]; // the top position of the furthest whole slot
5487 if (slatRemainder
) { // time spans part-way into the slot
5488 slatBottom
= this.slatTops
[slatIndex
+ 1];
5489 return slatTop
+ (slatBottom
- slatTop
) * slatRemainder
; // part-way between slots
5497 // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
5498 // Includes the the bottom of the last slat as the last item in the array.
5499 computeSlatTops: function() {
5503 this.slatEls
.each(function(i
, node
) {
5504 top
= $(node
).position().top
;
5508 tops
.push(top
+ this.slatEls
.last().outerHeight()); // bottom of the last slat
5510 this.slatTops
= tops
;
5514 /* Event Drag Visualization
5515 ------------------------------------------------------------------------------------------------------------------*/
5518 // Renders a visual indication of an event being dragged over the specified date(s).
5519 // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info.
5520 // A returned value of `true` signals that a mock "helper" event has been rendered.
5521 renderDrag: function(dropLocation
, seg
) {
5524 if (seg
) { // if there is event information for this drag, render a helper event
5525 this.renderRangeHelper(dropLocation
, seg
);
5527 opacity
= this.view
.opt('dragOpacity');
5528 if (opacity
!== undefined) {
5529 this.helperEl
.css('opacity', opacity
);
5532 return true; // signal that a helper has been rendered
5535 // otherwise, just render a highlight
5536 this.renderHighlight(
5537 this.view
.calendar
.ensureVisibleEventRange(dropLocation
) // needs to be a proper range
5543 // Unrenders any visual indication of an event being dragged
5544 destroyDrag: function() {
5545 this.destroyHelper();
5546 this.destroyHighlight();
5550 /* Event Resize Visualization
5551 ------------------------------------------------------------------------------------------------------------------*/
5554 // Renders a visual indication of an event being resized
5555 renderEventResize: function(range
, seg
) {
5556 this.renderRangeHelper(range
, seg
);
5560 // Unrenders any visual indication of an event being resized
5561 destroyEventResize: function() {
5562 this.destroyHelper();
5567 ------------------------------------------------------------------------------------------------------------------*/
5570 // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
5571 renderHelper: function(event
, sourceSeg
) {
5572 var segs
= this.eventsToSegs([ event
]);
5577 segs
= this.renderFgSegEls(segs
); // assigns each seg's el and returns a subset of segs that were rendered
5578 tableEl
= this.renderSegTable(segs
);
5580 // Try to make the segment that is in the same row as sourceSeg look the same
5581 for (i
= 0; i
< segs
.length
; i
++) {
5583 if (sourceSeg
&& sourceSeg
.col
=== seg
.col
) {
5584 sourceEl
= sourceSeg
.el
;
5586 left
: sourceEl
.css('left'),
5587 right
: sourceEl
.css('right'),
5588 'margin-left': sourceEl
.css('margin-left'),
5589 'margin-right': sourceEl
.css('margin-right')
5594 this.helperEl
= $('<div class="fc-helper-skeleton"/>')
5600 // Unrenders any mock helper event
5601 destroyHelper: function() {
5602 if (this.helperEl
) {
5603 this.helperEl
.remove();
5604 this.helperEl
= null;
5610 ------------------------------------------------------------------------------------------------------------------*/
5613 // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
5614 renderSelection: function(range
) {
5615 if (this.view
.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
5616 this.renderRangeHelper(range
);
5619 this.renderHighlight(range
);
5624 // Unrenders any visual indication of a selection
5625 destroySelection: function() {
5626 this.destroyHelper();
5627 this.destroyHighlight();
5631 /* Fill System (highlight, background events, business hours)
5632 ------------------------------------------------------------------------------------------------------------------*/
5635 // Renders a set of rectangles over the given time segments.
5636 // Only returns segments that successfully rendered.
5637 renderFill: function(type
, segs
, className
) {
5649 segs
= this.renderFillSegEls(type
, segs
); // assignes `.el` to each seg. returns successfully rendered segs
5650 segCols
= this.groupSegCols(segs
); // group into sub-arrays, and assigns 'col' to each seg
5652 className
= className
|| type
.toLowerCase();
5654 '<div class="fc-' + className
+ '-skeleton">' +
5655 '<table><tr/></table>' +
5658 trEl
= skeletonEl
.find('tr');
5660 for (col
= 0; col
< segCols
.length
; col
++) {
5661 colSegs
= segCols
[col
];
5662 tdEl
= $('<td/>').appendTo(trEl
);
5664 if (colSegs
.length
) {
5665 containerEl
= $('<div class="fc-' + className
+ '-container"/>').appendTo(tdEl
);
5666 dayDate
= this.colData
[col
].day
;
5668 for (i
= 0; i
< colSegs
.length
; i
++) {
5672 top
: this.computeDateTop(seg
.start
, dayDate
),
5673 bottom
: -this.computeDateTop(seg
.end
, dayDate
) // the y position of the bottom edge
5680 this.bookendCells(trEl
, type
);
5682 this.el
.append(skeletonEl
);
5683 this.elsByFill
[type
] = skeletonEl
;
5691 /* Event-rendering methods for the TimeGrid class
5692 ----------------------------------------------------------------------------------------------------------------------*/
5696 eventSkeletonEl
: null, // has cells with event-containers, which contain absolutely positioned event elements
5699 // Renders the given foreground event segments onto the grid
5700 renderFgSegs: function(segs
) {
5701 segs
= this.renderFgSegEls(segs
); // returns a subset of the segs. segs that were actually rendered
5704 this.eventSkeletonEl
= $('<div class="fc-content-skeleton"/>')
5705 .append(this.renderSegTable(segs
))
5708 return segs
; // return only the segs that were actually rendered
5712 // Unrenders all currently rendered foreground event segments
5713 destroyFgSegs: function(segs
) {
5714 if (this.eventSkeletonEl
) {
5715 this.eventSkeletonEl
.remove();
5716 this.eventSkeletonEl
= null;
5721 // Renders and returns the <table> portion of the event-skeleton.
5722 // Returns an object with properties 'tbodyEl' and 'segs'.
5723 renderSegTable: function(segs
) {
5724 var tableEl
= $('<table><tr/></table>');
5725 var trEl
= tableEl
.find('tr');
5731 segCols
= this.groupSegCols(segs
); // group into sub-arrays, and assigns 'col' to each seg
5733 this.computeSegVerticals(segs
); // compute and assign top/bottom
5735 for (col
= 0; col
< segCols
.length
; col
++) { // iterate each column grouping
5736 colSegs
= segCols
[col
];
5737 placeSlotSegs(colSegs
); // compute horizontal coordinates, z-index's, and reorder the array
5739 containerEl
= $('<div class="fc-event-container"/>');
5741 // assign positioning CSS and insert into container
5742 for (i
= 0; i
< colSegs
.length
; i
++) {
5744 seg
.el
.css(this.generateSegPositionCss(seg
));
5746 // if the height is short, add a className for alternate styling
5747 if (seg
.bottom
- seg
.top
< 30) {
5748 seg
.el
.addClass('fc-short');
5751 containerEl
.append(seg
.el
);
5754 trEl
.append($('<td/>').append(containerEl
));
5757 this.bookendCells(trEl
, 'eventSkeleton');
5763 // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
5764 // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
5765 updateSegVerticals: function() {
5766 var allSegs
= (this.segs
|| []).concat(this.businessHourSegs
|| []);
5769 this.computeSegVerticals(allSegs
);
5771 for (i
= 0; i
< allSegs
.length
; i
++) {
5773 this.generateSegVerticalCss(allSegs
[i
])
5779 // For each segment in an array, computes and assigns its top and bottom properties
5780 computeSegVerticals: function(segs
) {
5783 for (i
= 0; i
< segs
.length
; i
++) {
5785 seg
.top
= this.computeDateTop(seg
.start
, seg
.start
);
5786 seg
.bottom
= this.computeDateTop(seg
.end
, seg
.start
);
5791 // Renders the HTML for a single event segment's default rendering
5792 fgSegHtml: function(seg
, disableResizing
) {
5793 var view
= this.view
;
5794 var event
= seg
.event
;
5795 var isDraggable
= view
.isEventDraggable(event
);
5796 var isResizable
= !disableResizing
&& seg
.isEnd
&& view
.isEventResizable(event
);
5797 var classes
= this.getSegClasses(seg
, isDraggable
, isResizable
);
5798 var skinCss
= this.getEventSkinCss(event
);
5800 var fullTimeText
; // more verbose time text. for the print stylesheet
5801 var startTimeText
; // just the start time text
5803 classes
.unshift('fc-time-grid-event');
5805 if (view
.isMultiDayEvent(event
)) { // if the event appears to span more than one day...
5806 // Don't display time text on segments that run entirely through a day.
5807 // That would appear as midnight-midnight and would look dumb.
5808 // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
5809 if (seg
.isStart
|| seg
.isEnd
) {
5810 timeText
= this.getEventTimeText(seg
);
5811 fullTimeText
= this.getEventTimeText(seg
, 'LT');
5812 startTimeText
= this.getEventTimeText({ start
: seg
.start
});
5815 // Display the normal time text for the *event's* times
5816 timeText
= this.getEventTimeText(event
);
5817 fullTimeText
= this.getEventTimeText(event
, 'LT');
5818 startTimeText
= this.getEventTimeText({ start
: event
.start
});
5821 return '<a class="' + classes
.join(' ') + '"' +
5823 ' href="' + htmlEscape(event
.url
) + '"' :
5827 ' style="' + skinCss
+ '"' :
5831 '<div class="fc-content">' +
5833 '<div class="fc-time"' +
5834 ' data-start="' + htmlEscape(startTimeText
) + '"' +
5835 ' data-full="' + htmlEscape(fullTimeText
) + '"' +
5837 '<span>' + htmlEscape(timeText
) + '</span>' +
5842 '<div class="fc-title">' +
5843 htmlEscape(event
.title
) +
5848 '<div class="fc-bg"/>' +
5850 '<div class="fc-resizer"/>' :
5857 // Generates an object with CSS properties/values that should be applied to an event segment element.
5858 // Contains important positioning-related properties that should be applied to any event element, customized or not.
5859 generateSegPositionCss: function(seg
) {
5860 var shouldOverlap
= this.view
.opt('slotEventOverlap');
5861 var backwardCoord
= seg
.backwardCoord
; // the left side if LTR. the right side if RTL. floating-point
5862 var forwardCoord
= seg
.forwardCoord
; // the right side if LTR. the left side if RTL. floating-point
5863 var props
= this.generateSegVerticalCss(seg
); // get top/bottom first
5864 var left
; // amount of space from left edge, a fraction of the total width
5865 var right
; // amount of space from right edge, a fraction of the total width
5867 if (shouldOverlap
) {
5868 // double the width, but don't go beyond the maximum forward coordinate (1.0)
5869 forwardCoord
= Math
.min(1, backwardCoord
+ (forwardCoord
- backwardCoord
) * 2);
5873 left
= 1 - forwardCoord
;
5874 right
= backwardCoord
;
5877 left
= backwardCoord
;
5878 right
= 1 - forwardCoord
;
5881 props
.zIndex
= seg
.level
+ 1; // convert from 0-base to 1-based
5882 props
.left
= left
* 100 + '%';
5883 props
.right
= right
* 100 + '%';
5885 if (shouldOverlap
&& seg
.forwardPressure
) {
5886 // add padding to the edge so that forward stacked events don't cover the resizer's icon
5887 props
[this.isRTL
? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
5894 // Generates an object with CSS properties for the top/bottom coordinates of a segment element
5895 generateSegVerticalCss: function(seg
) {
5898 bottom
: -seg
.bottom
// flipped because needs to be space beyond bottom edge of event container
5903 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
5904 groupSegCols: function(segs
) {
5908 for (i
= 0; i
< this.colCnt
; i
++) {
5912 for (i
= 0; i
< segs
.length
; i
++) {
5913 segCols
[segs
[i
].col
].push(segs
[i
]);
5922 // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
5923 // Also reorders the given array by date!
5924 function placeSlotSegs(segs
) {
5929 segs
.sort(compareSegs
); // order by date
5930 levels
= buildSlotSegLevels(segs
);
5931 computeForwardSlotSegs(levels
);
5933 if ((level0
= levels
[0])) {
5935 for (i
= 0; i
< level0
.length
; i
++) {
5936 computeSlotSegPressures(level0
[i
]);
5939 for (i
= 0; i
< level0
.length
; i
++) {
5940 computeSlotSegCoords(level0
[i
], 0, 0);
5946 // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
5947 // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
5948 function buildSlotSegLevels(segs
) {
5953 for (i
=0; i
<segs
.length
; i
++) {
5956 // go through all the levels and stop on the first level where there are no collisions
5957 for (j
=0; j
<levels
.length
; j
++) {
5958 if (!computeSlotSegCollisions(seg
, levels
[j
]).length
) {
5965 (levels
[j
] || (levels
[j
] = [])).push(seg
);
5972 // For every segment, figure out the other segments that are in subsequent
5973 // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
5974 function computeForwardSlotSegs(levels
) {
5979 for (i
=0; i
<levels
.length
; i
++) {
5982 for (j
=0; j
<level
.length
; j
++) {
5985 seg
.forwardSegs
= [];
5986 for (k
=i
+1; k
<levels
.length
; k
++) {
5987 computeSlotSegCollisions(seg
, levels
[k
], seg
.forwardSegs
);
5994 // Figure out which path forward (via seg.forwardSegs) results in the longest path until
5995 // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
5996 function computeSlotSegPressures(seg
) {
5997 var forwardSegs
= seg
.forwardSegs
;
5998 var forwardPressure
= 0;
6001 if (seg
.forwardPressure
=== undefined) { // not already computed
6003 for (i
=0; i
<forwardSegs
.length
; i
++) {
6004 forwardSeg
= forwardSegs
[i
];
6006 // figure out the child's maximum forward path
6007 computeSlotSegPressures(forwardSeg
);
6009 // either use the existing maximum, or use the child's forward pressure
6010 // plus one (for the forwardSeg itself)
6011 forwardPressure
= Math
.max(
6013 1 + forwardSeg
.forwardPressure
6017 seg
.forwardPressure
= forwardPressure
;
6022 // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
6023 // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
6024 // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
6026 // The segment might be part of a "series", which means consecutive segments with the same pressure
6027 // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
6028 // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
6029 // coordinate of the first segment in the series.
6030 function computeSlotSegCoords(seg
, seriesBackwardPressure
, seriesBackwardCoord
) {
6031 var forwardSegs
= seg
.forwardSegs
;
6034 if (seg
.forwardCoord
=== undefined) { // not already computed
6036 if (!forwardSegs
.length
) {
6038 // if there are no forward segments, this segment should butt up against the edge
6039 seg
.forwardCoord
= 1;
6043 // sort highest pressure first
6044 forwardSegs
.sort(compareForwardSlotSegs
);
6046 // this segment's forwardCoord will be calculated from the backwardCoord of the
6047 // highest-pressure forward segment.
6048 computeSlotSegCoords(forwardSegs
[0], seriesBackwardPressure
+ 1, seriesBackwardCoord
);
6049 seg
.forwardCoord
= forwardSegs
[0].backwardCoord
;
6052 // calculate the backwardCoord from the forwardCoord. consider the series
6053 seg
.backwardCoord
= seg
.forwardCoord
-
6054 (seg
.forwardCoord
- seriesBackwardCoord
) / // available width for series
6055 (seriesBackwardPressure
+ 1); // # of segments in the series
6057 // use this segment's coordinates to computed the coordinates of the less-pressurized
6059 for (i
=0; i
<forwardSegs
.length
; i
++) {
6060 computeSlotSegCoords(forwardSegs
[i
], 0, seg
.forwardCoord
);
6066 // Find all the segments in `otherSegs` that vertically collide with `seg`.
6067 // Append into an optionally-supplied `results` array and return.
6068 function computeSlotSegCollisions(seg
, otherSegs
, results
) {
6069 results
= results
|| [];
6071 for (var i
=0; i
<otherSegs
.length
; i
++) {
6072 if (isSlotSegCollision(seg
, otherSegs
[i
])) {
6073 results
.push(otherSegs
[i
]);
6081 // Do these segments occupy the same vertical space?
6082 function isSlotSegCollision(seg1
, seg2
) {
6083 return seg1
.bottom
> seg2
.top
&& seg1
.top
< seg2
.bottom
;
6087 // A cmp function for determining which forward segment to rely on more when computing coordinates.
6088 function compareForwardSlotSegs(seg1
, seg2
) {
6089 // put higher-pressure first
6090 return seg2
.forwardPressure
- seg1
.forwardPressure
||
6091 // put segments that are closer to initial edge first (and favor ones with no coords yet)
6092 (seg1
.backwardCoord
|| 0) - (seg2
.backwardCoord
|| 0) ||
6093 // do normal sorting...
6094 compareSegs(seg1
, seg2
);
6097 /* An abstract class from which other views inherit from
6098 ----------------------------------------------------------------------------------------------------------------------*/
6100 var View
= fc
.View
= Class
.extend({
6102 type
: null, // subclass' view name (string)
6103 name
: null, // deprecated. use `type` instead
6105 calendar
: null, // owner Calendar object
6106 options
: null, // view-specific options
6107 coordMap
: null, // a CoordMap object for converting pixel regions to dates
6108 el
: null, // the view's containing element. set by Calendar
6110 // range the view is actually displaying (moments)
6112 end
: null, // exclusive
6114 // range the view is formally responsible for (moments)
6115 // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
6116 intervalStart
: null,
6117 intervalEnd
: null, // exclusive
6119 intervalDuration
: null, // the whole-unit duration that is being displayed
6120 intervalUnit
: null, // name of largest unit being displayed, like "month" or "week"
6122 isSelected
: false, // boolean whether a range of time is user-selected or not
6124 // subclasses can optionally use a scroll container
6125 scrollerEl
: null, // the element that will most likely scroll when content is too tall
6126 scrollTop
: null, // cached vertical scroll value
6128 // classNames styled by jqui themes
6129 widgetHeaderClass
: null,
6130 widgetContentClass
: null,
6131 highlightStateClass
: null,
6133 // for date utils, computed from options
6134 nextDayThreshold
: null,
6135 isHiddenDayHash
: null,
6137 // document handlers, bound to `this` object
6138 documentMousedownProxy
: null, // TODO: doesn't work with touch
6141 constructor: function(calendar
, viewOptions
, viewType
) {
6142 this.calendar
= calendar
;
6143 this.options
= viewOptions
;
6144 this.type
= this.name
= viewType
; // .name is deprecated
6146 this.nextDayThreshold
= moment
.duration(this.opt('nextDayThreshold'));
6148 this.initHiddenDays();
6150 this.documentMousedownProxy
= $.proxy(this, 'documentMousedown');
6156 // A good place for subclasses to initialize member variables
6157 initialize: function() {
6158 // subclasses can implement
6162 // Retrieves an option with the given name
6163 opt: function(name
) {
6166 val
= this.options
[name
]; // look at view-specific options first
6167 if (val
!== undefined) {
6171 val
= this.calendar
.options
[name
];
6172 if ($.isPlainObject(val
) && !isForcedAtomicOption(name
)) { // view-option-hashes are deprecated
6173 return smartProperty(val
, this.type
);
6180 // Triggers handlers that are view-related. Modifies args before passing to calendar.
6181 trigger: function(name
, thisObj
) { // arguments beyond thisObj are passed along
6182 var calendar
= this.calendar
;
6184 return calendar
.trigger
.apply(
6186 [name
, thisObj
|| this].concat(
6187 Array
.prototype.slice
.call(arguments
, 2), // arguments beyond thisObj
6188 [ this ] // always make the last argument a reference to the view. TODO: deprecate
6195 ------------------------------------------------------------------------------------------------------------------*/
6198 // Updates all internal dates to center around the given current date
6199 setDate: function(date
) {
6200 this.setRange(this.computeRange(date
));
6204 // Updates all internal dates for displaying the given range.
6205 // Expects all values to be normalized (like what computeRange does).
6206 setRange: function(range
) {
6207 $.extend(this, range
);
6211 // Given a single current date, produce information about what range to display.
6212 // Subclasses can override. Must return all properties.
6213 computeRange: function(date
) {
6214 var intervalDuration
= moment
.duration(this.opt('duration') || this.constructor.duration
|| { days
: 1 });
6215 var intervalUnit
= computeIntervalUnit(intervalDuration
);
6216 var intervalStart
= date
.clone().startOf(intervalUnit
);
6217 var intervalEnd
= intervalStart
.clone().add(intervalDuration
);
6220 // normalize the range's time-ambiguity
6221 if (computeIntervalAs('days', intervalDuration
)) { // whole-days?
6222 intervalStart
.stripTime();
6223 intervalEnd
.stripTime();
6225 else { // needs to have a time?
6226 if (!intervalStart
.hasTime()) {
6227 intervalStart
= this.calendar
.rezoneDate(intervalStart
); // convert to current timezone, with 00:00
6229 if (!intervalEnd
.hasTime()) {
6230 intervalEnd
= this.calendar
.rezoneDate(intervalEnd
); // convert to current timezone, with 00:00
6234 start
= intervalStart
.clone();
6235 start
= this.skipHiddenDays(start
);
6236 end
= intervalEnd
.clone();
6237 end
= this.skipHiddenDays(end
, -1, true); // exclusively move backwards
6240 intervalDuration
: intervalDuration
,
6241 intervalUnit
: intervalUnit
,
6242 intervalStart
: intervalStart
,
6243 intervalEnd
: intervalEnd
,
6250 // Computes the new date when the user hits the prev button, given the current date
6251 computePrevDate: function(date
) {
6252 return this.skipHiddenDays(
6253 date
.clone().startOf(this.intervalUnit
).subtract(this.intervalDuration
), -1
6258 // Computes the new date when the user hits the next button, given the current date
6259 computeNextDate: function(date
) {
6260 return this.skipHiddenDays(
6261 date
.clone().startOf(this.intervalUnit
).add(this.intervalDuration
)
6266 /* Title and Date Formatting
6267 ------------------------------------------------------------------------------------------------------------------*/
6270 // Computes what the title at the top of the calendar should be for this view
6271 computeTitle: function() {
6272 return this.formatRange(
6273 { start
: this.intervalStart
, end
: this.intervalEnd
},
6274 this.opt('titleFormat') || this.computeTitleFormat(),
6275 this.opt('titleRangeSeparator')
6280 // Generates the format string that should be used to generate the title for the current date range.
6281 // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
6282 computeTitleFormat: function() {
6283 if (this.intervalUnit
== 'year') {
6286 else if (this.intervalUnit
== 'month') {
6287 return this.opt('monthYearFormat'); // like "September 2014"
6289 else if (this.intervalDuration
.as('days') > 1) {
6290 return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
6293 return 'LL'; // one day. longer, like "September 9 2014"
6298 // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
6299 // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
6300 formatRange: function(range
, formatStr
, separator
) {
6301 var end
= range
.end
;
6303 if (!end
.hasTime()) { // all-day?
6304 end
= end
.clone().subtract(1); // convert to inclusive. last ms of previous day
6307 return formatRange(range
.start
, end
, formatStr
, separator
, this.opt('isRTL'));
6312 ------------------------------------------------------------------------------------------------------------------*/
6315 // Wraps the basic render() method with more View-specific logic. Called by the owner Calendar.
6316 renderView: function() {
6319 this.initializeScroll();
6320 this.trigger('viewRender', this, this, this.el
);
6322 // attach handlers to document. do it here to allow for destroy/rerender
6323 $(document
).on('mousedown', this.documentMousedownProxy
);
6327 // Renders the view inside an already-defined `this.el`
6328 render: function() {
6329 // subclasses should implement
6333 // Wraps the basic destroy() method with more View-specific logic. Called by the owner Calendar.
6334 destroyView: function() {
6336 this.destroyViewEvents();
6338 this.trigger('viewDestroy', this, this, this.el
);
6340 $(document
).off('mousedown', this.documentMousedownProxy
);
6344 // Clears the view's rendering
6345 destroy: function() {
6346 this.el
.empty(); // removes inner contents but leaves the element intact
6350 // Initializes internal variables related to theming
6351 initTheming: function() {
6352 var tm
= this.opt('theme') ? 'ui' : 'fc';
6354 this.widgetHeaderClass
= tm
+ '-widget-header';
6355 this.widgetContentClass
= tm
+ '-widget-content';
6356 this.highlightStateClass
= tm
+ '-state-highlight';
6361 ------------------------------------------------------------------------------------------------------------------*/
6364 // Refreshes anything dependant upon sizing of the container element of the grid
6365 updateSize: function(isResize
) {
6367 this.recordScroll();
6369 this.updateHeight();
6374 // Refreshes the horizontal dimensions of the calendar
6375 updateWidth: function() {
6376 // subclasses should implement
6380 // Refreshes the vertical dimensions of the calendar
6381 updateHeight: function() {
6382 var calendar
= this.calendar
; // we poll the calendar for height information
6385 calendar
.getSuggestedViewHeight(),
6386 calendar
.isHeightAuto()
6391 // Updates the vertical dimensions of the calendar to the specified height.
6392 // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
6393 setHeight: function(height
, isAuto
) {
6394 // subclasses should implement
6399 ------------------------------------------------------------------------------------------------------------------*/
6402 // Given the total height of the view, return the number of pixels that should be used for the scroller.
6403 // By default, uses this.scrollerEl, but can pass this in as well.
6404 // Utility for subclasses.
6405 computeScrollerHeight: function(totalHeight
, scrollerEl
) {
6407 var otherHeight
; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
6409 scrollerEl
= scrollerEl
|| this.scrollerEl
;
6410 both
= this.el
.add(scrollerEl
);
6412 // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
6414 position
: 'relative', // cause a reflow, which will force fresh dimension recalculation
6415 left
: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
6417 otherHeight
= this.el
.outerHeight() - scrollerEl
.height(); // grab the dimensions
6418 both
.css({ position
: '', left
: '' }); // undo hack
6420 return totalHeight
- otherHeight
;
6424 // Sets the scroll value of the scroller to the initial pre-configured state prior to allowing the user to change it
6425 initializeScroll: function() {
6429 // Called for remembering the current scroll value of the scroller.
6430 // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
6431 // change the scroll of the container.
6432 recordScroll: function() {
6433 if (this.scrollerEl
) {
6434 this.scrollTop
= this.scrollerEl
.scrollTop();
6439 // Set the scroll value of the scroller to the previously recorded value.
6440 // Should be called after we know the view's dimensions have been restored following some type of destructive
6441 // operation (like temporarily removing DOM elements).
6442 restoreScroll: function() {
6443 if (this.scrollTop
!== null) {
6444 this.scrollerEl
.scrollTop(this.scrollTop
);
6449 /* Event Elements / Segments
6450 ------------------------------------------------------------------------------------------------------------------*/
6453 // Wraps the basic renderEvents() method with more View-specific logic
6454 renderViewEvents: function(events
) {
6455 this.renderEvents(events
);
6457 this.eventSegEach(function(seg
) {
6458 this.trigger('eventAfterRender', seg
.event
, seg
.event
, seg
.el
);
6460 this.trigger('eventAfterAllRender');
6464 // Renders the events onto the view.
6465 renderEvents: function() {
6466 // subclasses should implement
6470 // Wraps the basic destroyEvents() method with more View-specific logic
6471 destroyViewEvents: function() {
6472 this.eventSegEach(function(seg
) {
6473 this.trigger('eventDestroy', seg
.event
, seg
.event
, seg
.el
);
6476 this.destroyEvents();
6480 // Removes event elements from the view.
6481 destroyEvents: function() {
6482 // subclasses should implement
6486 // Given an event and the default element used for rendering, returns the element that should actually be used.
6487 // Basically runs events and elements through the eventRender hook.
6488 resolveEventEl: function(event
, el
) {
6489 var custom
= this.trigger('eventRender', event
, event
, el
);
6491 if (custom
=== false) { // means don't render at all
6494 else if (custom
&& custom
!== true) {
6502 // Hides all rendered event segments linked to the given event
6503 showEvent: function(event
) {
6504 this.eventSegEach(function(seg
) {
6505 seg
.el
.css('visibility', '');
6510 // Shows all rendered event segments linked to the given event
6511 hideEvent: function(event
) {
6512 this.eventSegEach(function(seg
) {
6513 seg
.el
.css('visibility', 'hidden');
6518 // Iterates through event segments. Goes through all by default.
6519 // If the optional `event` argument is specified, only iterates through segments linked to that event.
6520 // The `this` value of the callback function will be the view.
6521 eventSegEach: function(func
, event
) {
6522 var segs
= this.getEventSegs();
6525 for (i
= 0; i
< segs
.length
; i
++) {
6526 if (!event
|| segs
[i
].event
._id
=== event
._id
) {
6527 func
.call(this, segs
[i
]);
6533 // Retrieves all the rendered segment objects for the view
6534 getEventSegs: function() {
6535 // subclasses must implement
6540 /* Event Drag-n-Drop
6541 ------------------------------------------------------------------------------------------------------------------*/
6544 // Computes if the given event is allowed to be dragged by the user
6545 isEventDraggable: function(event
) {
6546 var source
= event
.source
|| {};
6548 return firstDefined(
6549 event
.startEditable
,
6550 source
.startEditable
,
6551 this.opt('eventStartEditable'),
6554 this.opt('editable')
6559 // Must be called when an event in the view is dropped onto new location.
6560 // `dropLocation` is an object that contains the new start/end/allDay values for the event.
6561 reportEventDrop: function(event
, dropLocation
, el
, ev
) {
6562 var calendar
= this.calendar
;
6563 var mutateResult
= calendar
.mutateEvent(event
, dropLocation
);
6564 var undoFunc = function() {
6565 mutateResult
.undo();
6566 calendar
.reportEventChange();
6569 this.triggerEventDrop(event
, mutateResult
.dateDelta
, undoFunc
, el
, ev
);
6570 calendar
.reportEventChange(); // will rerender events
6574 // Triggers event-drop handlers that have subscribed via the API
6575 triggerEventDrop: function(event
, dateDelta
, undoFunc
, el
, ev
) {
6576 this.trigger('eventDrop', el
[0], event
, dateDelta
, undoFunc
, ev
, {}); // {} = jqui dummy
6580 /* External Element Drag-n-Drop
6581 ------------------------------------------------------------------------------------------------------------------*/
6584 // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
6585 // `meta` is the parsed data that has been embedded into the dragging event.
6586 // `dropLocation` is an object that contains the new start/end/allDay values for the event.
6587 reportExternalDrop: function(meta
, dropLocation
, el
, ev
, ui
) {
6588 var eventProps
= meta
.eventProps
;
6592 // Try to build an event object and render it. TODO: decouple the two
6594 eventInput
= $.extend({}, eventProps
, dropLocation
);
6595 event
= this.calendar
.renderEvent(eventInput
, meta
.stick
)[0]; // renderEvent returns an array
6598 this.triggerExternalDrop(event
, dropLocation
, el
, ev
, ui
);
6602 // Triggers external-drop handlers that have subscribed via the API
6603 triggerExternalDrop: function(event
, dropLocation
, el
, ev
, ui
) {
6605 // trigger 'drop' regardless of whether element represents an event
6606 this.trigger('drop', el
[0], dropLocation
.start
, ev
, ui
);
6609 this.trigger('eventReceive', null, event
); // signal an external event landed
6614 /* Drag-n-Drop Rendering (for both events and external elements)
6615 ------------------------------------------------------------------------------------------------------------------*/
6618 // Renders a visual indication of a event or external-element drag over the given drop zone.
6619 // If an external-element, seg will be `null`
6620 renderDrag: function(dropLocation
, seg
) {
6621 // subclasses must implement
6625 // Unrenders a visual indication of an event or external-element being dragged.
6626 destroyDrag: function() {
6627 // subclasses must implement
6632 ------------------------------------------------------------------------------------------------------------------*/
6635 // Computes if the given event is allowed to be resize by the user
6636 isEventResizable: function(event
) {
6637 var source
= event
.source
|| {};
6639 return firstDefined(
6640 event
.durationEditable
,
6641 source
.durationEditable
,
6642 this.opt('eventDurationEditable'),
6645 this.opt('editable')
6650 // Must be called when an event in the view has been resized to a new length
6651 reportEventResize: function(event
, newEnd
, el
, ev
) {
6652 var calendar
= this.calendar
;
6653 var mutateResult
= calendar
.mutateEvent(event
, { end
: newEnd
});
6654 var undoFunc = function() {
6655 mutateResult
.undo();
6656 calendar
.reportEventChange();
6659 this.triggerEventResize(event
, mutateResult
.durationDelta
, undoFunc
, el
, ev
);
6660 calendar
.reportEventChange(); // will rerender events
6664 // Triggers event-resize handlers that have subscribed via the API
6665 triggerEventResize: function(event
, durationDelta
, undoFunc
, el
, ev
) {
6666 this.trigger('eventResize', el
[0], event
, durationDelta
, undoFunc
, ev
, {}); // {} = jqui dummy
6671 ------------------------------------------------------------------------------------------------------------------*/
6674 // Selects a date range on the view. `start` and `end` are both Moments.
6675 // `ev` is the native mouse event that begin the interaction.
6676 select: function(range
, ev
) {
6678 this.renderSelection(range
);
6679 this.reportSelection(range
, ev
);
6683 // Renders a visual indication of the selection
6684 renderSelection: function(range
) {
6685 // subclasses should implement
6689 // Called when a new selection is made. Updates internal state and triggers handlers.
6690 reportSelection: function(range
, ev
) {
6691 this.isSelected
= true;
6692 this.trigger('select', null, range
.start
, range
.end
, ev
);
6696 // Undoes a selection. updates in the internal state and triggers handlers.
6697 // `ev` is the native mouse event that began the interaction.
6698 unselect: function(ev
) {
6699 if (this.isSelected
) {
6700 this.isSelected
= false;
6701 this.destroySelection();
6702 this.trigger('unselect', null, ev
);
6707 // Unrenders a visual indication of selection
6708 destroySelection: function() {
6709 // subclasses should implement
6713 // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
6714 documentMousedown: function(ev
) {
6717 // is there a selection, and has the user made a proper left click?
6718 if (this.isSelected
&& this.opt('unselectAuto') && isPrimaryMouseButton(ev
)) {
6720 // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
6721 ignore
= this.opt('unselectCancel');
6722 if (!ignore
|| !$(ev
.target
).closest(ignore
).length
) {
6730 ------------------------------------------------------------------------------------------------------------------*/
6733 // Initializes internal variables related to calculating hidden days-of-week
6734 initHiddenDays: function() {
6735 var hiddenDays
= this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
6736 var isHiddenDayHash
= []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
6740 if (this.opt('weekends') === false) {
6741 hiddenDays
.push(0, 6); // 0=sunday, 6=saturday
6744 for (i
= 0; i
< 7; i
++) {
6746 !(isHiddenDayHash
[i
] = $.inArray(i
, hiddenDays
) !== -1)
6753 throw 'invalid hiddenDays'; // all days were hidden? bad.
6756 this.isHiddenDayHash
= isHiddenDayHash
;
6760 // Is the current day hidden?
6761 // `day` is a day-of-week index (0-6), or a Moment
6762 isHiddenDay: function(day
) {
6763 if (moment
.isMoment(day
)) {
6766 return this.isHiddenDayHash
[day
];
6770 // Incrementing the current day until it is no longer a hidden day, returning a copy.
6771 // If the initial value of `date` is not a hidden day, don't do anything.
6772 // Pass `isExclusive` as `true` if you are dealing with an end date.
6773 // `inc` defaults to `1` (increment one day forward each time)
6774 skipHiddenDays: function(date
, inc
, isExclusive
) {
6775 var out
= date
.clone();
6778 this.isHiddenDayHash
[(out
.day() + (isExclusive
? inc
: 0) + 7) % 7]
6780 out
.add(inc
, 'days');
6786 // Returns the date range of the full days the given range visually appears to occupy.
6787 // Returns a new range object.
6788 computeDayRange: function(range
) {
6789 var startDay
= range
.start
.clone().stripTime(); // the beginning of the day the range starts
6790 var end
= range
.end
;
6795 endDay
= end
.clone().stripTime(); // the beginning of the day the range exclusively ends
6796 endTimeMS
= +end
.time(); // # of milliseconds into `endDay`
6798 // If the end time is actually inclusively part of the next day and is equal to or
6799 // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
6800 // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
6801 if (endTimeMS
&& endTimeMS
>= this.nextDayThreshold
) {
6802 endDay
.add(1, 'days');
6806 // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
6807 // assign the default duration of one day.
6808 if (!end
|| endDay
<= startDay
) {
6809 endDay
= startDay
.clone().add(1, 'days');
6812 return { start
: startDay
, end
: endDay
};
6816 // Does the given event visually appear to occupy more than one day?
6817 isMultiDayEvent: function(event
) {
6818 var range
= this.computeDayRange(event
); // event is range-ish
6820 return range
.end
.diff(range
.start
, 'days') > 1;
6825 function Calendar(element
, instanceOptions
) {
6830 // Build options object
6831 // -----------------------------------------------------------------------------------
6832 // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
6834 instanceOptions
= instanceOptions
|| {};
6836 var options
= mergeOptions({}, defaults
, instanceOptions
);
6839 // determine language options
6840 if (options
.lang
in langOptionHash
) {
6841 langOptions
= langOptionHash
[options
.lang
];
6844 langOptions
= langOptionHash
[defaults
.lang
];
6847 if (langOptions
) { // if language options exist, rebuild...
6848 options
= mergeOptions({}, defaults
, langOptions
, instanceOptions
);
6851 if (options
.isRTL
) { // is isRTL, rebuild...
6852 options
= mergeOptions({}, defaults
, rtlDefaults
, langOptions
|| {}, instanceOptions
);
6858 // -----------------------------------------------------------------------------------
6860 t
.options
= options
;
6862 t
.destroy
= destroy
;
6863 t
.refetchEvents
= refetchEvents
;
6864 t
.reportEvents
= reportEvents
;
6865 t
.reportEventChange
= reportEventChange
;
6866 t
.rerenderEvents
= renderEvents
; // `renderEvents` serves as a rerender. an API method
6867 t
.changeView
= changeView
;
6869 t
.unselect
= unselect
;
6872 t
.prevYear
= prevYear
;
6873 t
.nextYear
= nextYear
;
6875 t
.gotoDate
= gotoDate
;
6876 t
.incrementDate
= incrementDate
;
6878 t
.getDate
= getDate
;
6879 t
.getCalendar
= getCalendar
;
6880 t
.getView
= getView
;
6882 t
.trigger
= trigger
;
6883 t
.isValidViewType
= isValidViewType
;
6884 t
.getViewButtonText
= getViewButtonText
;
6888 // Language-data Internals
6889 // -----------------------------------------------------------------------------------
6890 // Apply overrides to the current language's data
6893 var localeData
= createObject( // make a cheap copy
6894 getMomentLocaleData(options
.lang
) // will fall back to en
6897 if (options
.monthNames
) {
6898 localeData
._months
= options
.monthNames
;
6900 if (options
.monthNamesShort
) {
6901 localeData
._monthsShort
= options
.monthNamesShort
;
6903 if (options
.dayNames
) {
6904 localeData
._weekdays
= options
.dayNames
;
6906 if (options
.dayNamesShort
) {
6907 localeData
._weekdaysShort
= options
.dayNamesShort
;
6909 if (options
.firstDay
!= null) {
6910 var _week
= createObject(localeData
._week
); // _week: { dow: # }
6911 _week
.dow
= options
.firstDay
;
6912 localeData
._week
= _week
;
6917 // Calendar-specific Date Utilities
6918 // -----------------------------------------------------------------------------------
6921 t
.defaultAllDayEventDuration
= moment
.duration(options
.defaultAllDayEventDuration
);
6922 t
.defaultTimedEventDuration
= moment
.duration(options
.defaultTimedEventDuration
);
6925 // Builds a moment using the settings of the current calendar: timezone and language.
6926 // Accepts anything the vanilla moment() constructor accepts.
6927 t
.moment = function() {
6930 if (options
.timezone
=== 'local') {
6931 mom
= fc
.moment
.apply(null, arguments
);
6933 // Force the moment to be local, because fc.moment doesn't guarantee it.
6934 if (mom
.hasTime()) { // don't give ambiguously-timed moments a local zone
6938 else if (options
.timezone
=== 'UTC') {
6939 mom
= fc
.moment
.utc
.apply(null, arguments
); // process as UTC
6942 mom
= fc
.moment
.parseZone
.apply(null, arguments
); // let the input decide the zone
6945 if ('_locale' in mom
) { // moment 2.8 and above
6946 mom
._locale
= localeData
;
6948 else { // pre-moment-2.8
6949 mom
._lang
= localeData
;
6956 // Returns a boolean about whether or not the calendar knows how to calculate
6957 // the timezone offset of arbitrary dates in the current timezone.
6958 t
.getIsAmbigTimezone = function() {
6959 return options
.timezone
!== 'local' && options
.timezone
!== 'UTC';
6963 // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
6964 // This will also give the date an unambiguous time.
6965 t
.rezoneDate = function(date
) {
6966 return t
.moment(date
.toArray());
6970 // Returns a moment for the current date, as defined by the client's computer,
6971 // or overridden by the `now` option.
6972 t
.getNow = function() {
6973 var now
= options
.now
;
6974 if (typeof now
=== 'function') {
6977 return t
.moment(now
);
6981 // Calculates the week number for a moment according to the calendar's
6982 // `weekNumberCalculation` setting.
6983 t
.calculateWeekNumber = function(mom
) {
6984 var calc
= options
.weekNumberCalculation
;
6986 if (typeof calc
=== 'function') {
6989 else if (calc
=== 'local') {
6992 else if (calc
.toUpperCase() === 'ISO') {
6993 return mom
.isoWeek();
6998 // Get an event's normalized end date. If not present, calculate it from the defaults.
6999 t
.getEventEnd = function(event
) {
7001 return event
.end
.clone();
7004 return t
.getDefaultEventEnd(event
.allDay
, event
.start
);
7009 // Given an event's allDay status and start date, return swhat its fallback end date should be.
7010 t
.getDefaultEventEnd = function(allDay
, start
) { // TODO: rename to computeDefaultEventEnd
7011 var end
= start
.clone();
7014 end
.stripTime().add(t
.defaultAllDayEventDuration
);
7017 end
.add(t
.defaultTimedEventDuration
);
7020 if (t
.getIsAmbigTimezone()) {
7021 end
.stripZone(); // we don't know what the tzo should be
7028 // Produces a human-readable string for the given duration.
7029 // Side-effect: changes the locale of the given duration.
7030 function humanizeDuration(duration
) {
7031 return (duration
.locale
|| duration
.lang
).call(duration
, options
.lang
) // works moment-pre-2.8
7038 // -----------------------------------------------------------------------------------
7041 EventManager
.call(t
, options
);
7042 var isFetchNeeded
= t
.isFetchNeeded
;
7043 var fetchEvents
= t
.fetchEvents
;
7048 // -----------------------------------------------------------------------------------
7051 var _element
= element
[0];
7055 var tm
; // for making theme classes
7056 var viewSpecCache
= {};
7058 var suggestedViewHeight
;
7059 var windowResizeProxy
; // wraps the windowResize function
7060 var ignoreWindowResize
= 0;
7067 // -----------------------------------------------------------------------------------
7070 if (options
.defaultDate
!= null) {
7071 date
= t
.moment(options
.defaultDate
);
7078 function render(inc
) {
7082 else if (elementVisible()) {
7083 // mainly for the public API
7090 function initialRender() {
7091 tm
= options
.theme
? 'ui' : 'fc';
7092 element
.addClass('fc');
7094 if (options
.isRTL
) {
7095 element
.addClass('fc-rtl');
7098 element
.addClass('fc-ltr');
7101 if (options
.theme
) {
7102 element
.addClass('ui-widget');
7105 element
.addClass('fc-unthemed');
7108 content
= $("<div class='fc-view-container'/>").prependTo(element
);
7110 header
= new Header(t
, options
);
7111 headerElement
= header
.render();
7112 if (headerElement
) {
7113 element
.prepend(headerElement
);
7116 changeView(options
.defaultView
);
7118 if (options
.handleWindowResize
) {
7119 windowResizeProxy
= debounce(windowResize
, options
.windowResizeDelay
); // prevents rapid calls
7120 $(window
).resize(windowResizeProxy
);
7125 function destroy() {
7128 currentView
.destroyView();
7133 element
.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
7135 $(window
).unbind('resize', windowResizeProxy
);
7139 function elementVisible() {
7140 return element
.is(':visible');
7146 // -----------------------------------------------------------------------------------
7149 function changeView(viewType
) {
7150 renderView(0, viewType
);
7154 // Renders a view because of a date change, view-type change, or for the first time
7155 function renderView(delta
, viewType
) {
7156 ignoreWindowResize
++;
7158 // if viewType is changing, destroy the old view
7159 if (currentView
&& viewType
&& currentView
.type
!== viewType
) {
7160 header
.deactivateButton(currentView
.type
);
7161 freezeContentHeight(); // prevent a scroll jump when view element is removed
7162 if (currentView
.start
) { // rendered before?
7163 currentView
.destroyView();
7165 currentView
.el
.remove();
7169 // if viewType changed, or the view was never created, create a fresh view
7170 if (!currentView
&& viewType
) {
7171 currentView
= instantiateView(viewType
);
7172 currentView
.el
= $("<div class='fc-view fc-" + viewType
+ "-view' />").appendTo(content
);
7173 header
.activateButton(viewType
);
7178 // let the view determine what the delta means
7180 date
= currentView
.computePrevDate(date
);
7182 else if (delta
> 0) {
7183 date
= currentView
.computeNextDate(date
);
7186 // render or rerender the view
7188 !currentView
.start
|| // never rendered before
7189 delta
|| // explicit date window change
7190 !date
.isWithin(currentView
.intervalStart
, currentView
.intervalEnd
) // implicit date window change
7192 if (elementVisible()) {
7194 freezeContentHeight();
7195 if (currentView
.start
) { // rendered before?
7196 currentView
.destroyView();
7198 currentView
.setDate(date
);
7199 currentView
.renderView();
7200 unfreezeContentHeight();
7202 // need to do this after View::render, so dates are calculated
7204 updateTodayButton();
7206 getAndRenderEvents();
7211 unfreezeContentHeight(); // undo any lone freezeContentHeight calls
7212 ignoreWindowResize
--;
7217 // View Instantiation
7218 // -----------------------------------------------------------------------------------
7221 // Given a view name for a custom view or a standard view, creates a ready-to-go View object
7222 function instantiateView(viewType
) {
7223 var spec
= getViewSpec(viewType
);
7225 return new spec
['class'](t
, spec
.options
, viewType
);
7229 // Gets information about how to create a view
7230 function getViewSpec(requestedViewType
) {
7231 var allDefaultButtonText
= options
.defaultButtonText
|| {};
7232 var allButtonText
= options
.buttonText
|| {};
7233 var hash
= options
.views
|| {}; // the `views` option object
7234 var viewType
= requestedViewType
;
7235 var viewOptionsChain
= [];
7238 var duration
, unit
, unitIsSingle
= false;
7241 if (viewSpecCache
[requestedViewType
]) {
7242 return viewSpecCache
[requestedViewType
];
7245 function processSpecInput(input
) {
7246 if (typeof input
=== 'function') {
7249 else if (typeof input
=== 'object') {
7250 $.extend(viewOptions
, input
);
7254 // iterate up a view's spec ancestor chain util we find a class to instantiate
7255 while (viewType
&& !viewClass
) {
7256 viewOptions
= {}; // only for this specific view in the ancestry
7257 processSpecInput(fcViews
[viewType
]); // $.fullCalendar.views, lower precedence
7258 processSpecInput(hash
[viewType
]); // options at initialization, higher precedence
7259 viewOptionsChain
.unshift(viewOptions
); // record older ancestors first
7260 viewType
= viewOptions
.type
;
7263 viewOptionsChain
.unshift({}); // jQuery's extend needs at least one arg
7264 viewOptions
= $.extend
.apply($, viewOptionsChain
); // combine all, newer ancestors overwritting old
7268 duration
= viewOptions
.duration
|| viewClass
.duration
;
7270 duration
= moment
.duration(duration
);
7271 unit
= computeIntervalUnit(duration
);
7272 unitIsSingle
= computeIntervalAs(unit
, duration
) === 1;
7275 // options that are specified per the view's duration, like "week" or "day"
7276 if (unitIsSingle
&& hash
[unit
]) {
7277 viewOptions
= $.extend({}, hash
[unit
], viewOptions
); // lowest priority
7280 // compute the final text for the button representing this view
7282 allButtonText
[requestedViewType
] || // init options, like "agendaWeek"
7283 (unitIsSingle
? allButtonText
[unit
] : null) || // init options, like "week"
7284 allDefaultButtonText
[requestedViewType
] || // lang data, like "agendaWeek"
7285 (unitIsSingle
? allDefaultButtonText
[unit
] : null) || // lang data, like "week"
7286 viewOptions
.buttonText
||
7287 viewClass
.buttonText
||
7288 (duration
? humanizeDuration(duration
) : null) ||
7291 return (viewSpecCache
[requestedViewType
] = {
7293 options
: viewOptions
,
7294 buttonText
: buttonText
7300 // Returns a boolean about whether the view is okay to instantiate at some point
7301 function isValidViewType(viewType
) {
7302 return Boolean(getViewSpec(viewType
));
7306 // Gets the text that should be displayed on a view's button in the header
7307 function getViewButtonText(viewType
) {
7308 var spec
= getViewSpec(viewType
);
7311 return spec
.buttonText
;
7318 // -----------------------------------------------------------------------------------
7321 t
.getSuggestedViewHeight = function() {
7322 if (suggestedViewHeight
=== undefined) {
7325 return suggestedViewHeight
;
7329 t
.isHeightAuto = function() {
7330 return options
.contentHeight
=== 'auto' || options
.height
=== 'auto';
7334 function updateSize(shouldRecalc
) {
7335 if (elementVisible()) {
7341 ignoreWindowResize
++;
7342 currentView
.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
7343 ignoreWindowResize
--;
7345 return true; // signal success
7350 function calcSize() {
7351 if (elementVisible()) {
7357 function _calcSize() { // assumes elementVisible
7358 if (typeof options
.contentHeight
=== 'number') { // exists and not 'auto'
7359 suggestedViewHeight
= options
.contentHeight
;
7361 else if (typeof options
.height
=== 'number') { // exists and not 'auto'
7362 suggestedViewHeight
= options
.height
- (headerElement
? headerElement
.outerHeight(true) : 0);
7365 suggestedViewHeight
= Math
.round(content
.width() / Math
.max(options
.aspectRatio
, .5));
7370 function windowResize(ev
) {
7372 !ignoreWindowResize
&&
7373 ev
.target
=== window
&& // so we don't process jqui "resize" events that have bubbled up
7374 currentView
.start
// view has already been rendered
7376 if (updateSize(true)) {
7377 currentView
.trigger('windowResize', _element
);
7384 /* Event Fetching/Rendering
7385 -----------------------------------------------------------------------------*/
7386 // TODO: going forward, most of this stuff should be directly handled by the view
7389 function refetchEvents() { // can be called as an API method
7390 destroyEvents(); // so that events are cleared before user starts waiting for AJAX
7391 fetchAndRenderEvents();
7395 function renderEvents() { // destroys old events if previously rendered
7396 if (elementVisible()) {
7397 freezeContentHeight();
7398 currentView
.destroyViewEvents(); // no performance cost if never rendered
7399 currentView
.renderViewEvents(events
);
7400 unfreezeContentHeight();
7405 function destroyEvents() {
7406 freezeContentHeight();
7407 currentView
.destroyViewEvents();
7408 unfreezeContentHeight();
7412 function getAndRenderEvents() {
7413 if (!options
.lazyFetching
|| isFetchNeeded(currentView
.start
, currentView
.end
)) {
7414 fetchAndRenderEvents();
7422 function fetchAndRenderEvents() {
7423 fetchEvents(currentView
.start
, currentView
.end
);
7424 // ... will call reportEvents
7425 // ... which will call renderEvents
7429 // called when event data arrives
7430 function reportEvents(_events
) {
7436 // called when a single event's data has been changed
7437 function reportEventChange() {
7444 -----------------------------------------------------------------------------*/
7447 function updateTitle() {
7448 header
.updateTitle(currentView
.computeTitle());
7452 function updateTodayButton() {
7453 var now
= t
.getNow();
7454 if (now
.isWithin(currentView
.intervalStart
, currentView
.intervalEnd
)) {
7455 header
.disableButton('today');
7458 header
.enableButton('today');
7465 -----------------------------------------------------------------------------*/
7468 function select(start
, end
) {
7470 start
= t
.moment(start
);
7472 end
= t
.moment(end
);
7474 else if (start
.hasTime()) {
7475 end
= start
.clone().add(t
.defaultTimedEventDuration
);
7478 end
= start
.clone().add(t
.defaultAllDayEventDuration
);
7481 currentView
.select({ start
: start
, end
: end
}); // accepts a range
7485 function unselect() { // safe to be called before renderView
7487 currentView
.unselect();
7494 -----------------------------------------------------------------------------*/
7507 function prevYear() {
7508 date
.add(-1, 'years');
7513 function nextYear() {
7514 date
.add(1, 'years');
7525 function gotoDate(dateInput
) {
7526 date
= t
.moment(dateInput
);
7531 function incrementDate(delta
) {
7532 date
.add(moment
.duration(delta
));
7537 // Forces navigation to a view for the given date.
7538 // `viewType` can be a specific view name or a generic one like "week" or "day".
7539 function zoomTo(newDate
, viewType
) {
7543 if (!viewType
|| !isValidViewType(viewType
)) { // a general view name, or "auto"
7544 viewType
= viewType
|| 'day';
7545 viewStr
= header
.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
7547 // try to match a general view name, like "week", against a specific one, like "agendaWeek"
7548 match
= viewStr
.match(new RegExp('\\w+' + capitaliseFirstLetter(viewType
)));
7550 // fall back to the day view being used in the header
7552 match
= viewStr
.match(/\w+Day/);
7555 viewType
= match
? match
[0] : 'agendaDay'; // fall back to agendaDay
7559 changeView(viewType
);
7563 function getDate() {
7564 return date
.clone();
7569 /* Height "Freezing"
7570 -----------------------------------------------------------------------------*/
7573 function freezeContentHeight() {
7576 height
: content
.height(),
7582 function unfreezeContentHeight() {
7593 -----------------------------------------------------------------------------*/
7596 function getCalendar() {
7601 function getView() {
7606 function option(name
, value
) {
7607 if (value
=== undefined) {
7608 return options
[name
];
7610 if (name
== 'height' || name
== 'contentHeight' || name
== 'aspectRatio') {
7611 options
[name
] = value
;
7612 updateSize(true); // true = allow recalculation of height
7617 function trigger(name
, thisObj
) {
7618 if (options
[name
]) {
7619 return options
[name
].apply(
7620 thisObj
|| _element
,
7621 Array
.prototype.slice
.call(arguments
, 2)
7628 /* Top toolbar area with buttons and title
7629 ----------------------------------------------------------------------------------------------------------------------*/
7630 // TODO: rename all header-related things to "toolbar"
7632 function Header(calendar
, options
) {
7637 t
.destroy
= destroy
;
7638 t
.updateTitle
= updateTitle
;
7639 t
.activateButton
= activateButton
;
7640 t
.deactivateButton
= deactivateButton
;
7641 t
.disableButton
= disableButton
;
7642 t
.enableButton
= enableButton
;
7643 t
.getViewsWithButtons
= getViewsWithButtons
;
7647 var viewsWithButtons
= [];
7652 var sections
= options
.header
;
7654 tm
= options
.theme
? 'ui' : 'fc';
7657 el
= $("<div class='fc-toolbar'/>")
7658 .append(renderSection('left'))
7659 .append(renderSection('right'))
7660 .append(renderSection('center'))
7661 .append('<div class="fc-clear"/>');
7668 function destroy() {
7673 function renderSection(position
) {
7674 var sectionEl
= $('<div class="fc-' + position
+ '"/>');
7675 var buttonStr
= options
.header
[position
];
7678 $.each(buttonStr
.split(' '), function(i
) {
7679 var groupChildren
= $();
7680 var isOnlyButtons
= true;
7683 $.each(this.split(','), function(j
, buttonName
) {
7688 var viewText
; // highest priority
7694 if (buttonName
== 'title') {
7695 groupChildren
= groupChildren
.add($('<h2> </h2>')); // we always want it to take up height
7696 isOnlyButtons
= false;
7699 if (calendar
[buttonName
]) { // a calendar method
7700 buttonClick = function() {
7701 calendar
[buttonName
]();
7704 else if (calendar
.isValidViewType(buttonName
)) { // a view type
7705 buttonClick = function() {
7706 calendar
.changeView(buttonName
);
7708 viewsWithButtons
.push(buttonName
);
7709 viewText
= calendar
.getViewButtonText(buttonName
);
7713 // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")
7714 themeIcon
= smartProperty(options
.themeButtonIcons
, buttonName
);
7715 normalIcon
= smartProperty(options
.buttonIcons
, buttonName
);
7716 defaultText
= smartProperty(options
.defaultButtonText
, buttonName
); // from languages
7717 customText
= smartProperty(options
.buttonText
, buttonName
);
7719 if (viewText
|| customText
) {
7720 innerHtml
= htmlEscape(viewText
|| customText
);
7722 else if (themeIcon
&& options
.theme
) {
7723 innerHtml
= "<span class='ui-icon ui-icon-" + themeIcon
+ "'></span>";
7725 else if (normalIcon
&& !options
.theme
) {
7726 innerHtml
= "<span class='fc-icon fc-icon-" + normalIcon
+ "'></span>";
7729 innerHtml
= htmlEscape(defaultText
|| buttonName
);
7733 'fc-' + buttonName
+ '-button',
7735 tm
+ '-state-default'
7738 button
= $( // type="button" so that it doesn't submit a form
7739 '<button type="button" class="' + classes
.join(' ') + '">' +
7744 // don't process clicks for disabled buttons
7745 if (!button
.hasClass(tm
+ '-state-disabled')) {
7749 // after the click action, if the button becomes the "active" tab, or disabled,
7750 // it should never have a hover class, so remove it now.
7752 button
.hasClass(tm
+ '-state-active') ||
7753 button
.hasClass(tm
+ '-state-disabled')
7755 button
.removeClass(tm
+ '-state-hover');
7759 .mousedown(function() {
7760 // the *down* effect (mouse pressed in).
7761 // only on buttons that are not the "active" tab, or disabled
7763 .not('.' + tm
+ '-state-active')
7764 .not('.' + tm
+ '-state-disabled')
7765 .addClass(tm
+ '-state-down');
7767 .mouseup(function() {
7768 // undo the *down* effect
7769 button
.removeClass(tm
+ '-state-down');
7773 // the *hover* effect.
7774 // only on buttons that are not the "active" tab, or disabled
7776 .not('.' + tm
+ '-state-active')
7777 .not('.' + tm
+ '-state-disabled')
7778 .addClass(tm
+ '-state-hover');
7781 // undo the *hover* effect
7783 .removeClass(tm
+ '-state-hover')
7784 .removeClass(tm
+ '-state-down'); // if mouseleave happens before mouseup
7788 groupChildren
= groupChildren
.add(button
);
7793 if (isOnlyButtons
) {
7795 .first().addClass(tm
+ '-corner-left').end()
7796 .last().addClass(tm
+ '-corner-right').end();
7799 if (groupChildren
.length
> 1) {
7800 groupEl
= $('<div/>');
7801 if (isOnlyButtons
) {
7802 groupEl
.addClass('fc-button-group');
7804 groupEl
.append(groupChildren
);
7805 sectionEl
.append(groupEl
);
7808 sectionEl
.append(groupChildren
); // 1 or 0 children
7817 function updateTitle(text
) {
7818 el
.find('h2').text(text
);
7822 function activateButton(buttonName
) {
7823 el
.find('.fc-' + buttonName
+ '-button')
7824 .addClass(tm
+ '-state-active');
7828 function deactivateButton(buttonName
) {
7829 el
.find('.fc-' + buttonName
+ '-button')
7830 .removeClass(tm
+ '-state-active');
7834 function disableButton(buttonName
) {
7835 el
.find('.fc-' + buttonName
+ '-button')
7836 .attr('disabled', 'disabled')
7837 .addClass(tm
+ '-state-disabled');
7841 function enableButton(buttonName
) {
7842 el
.find('.fc-' + buttonName
+ '-button')
7843 .removeAttr('disabled')
7844 .removeClass(tm
+ '-state-disabled');
7848 function getViewsWithButtons() {
7849 return viewsWithButtons
;
7854 fc
.sourceNormalizers
= [];
7855 fc
.sourceFetchers
= [];
7857 var ajaxDefaults
= {
7865 function EventManager(options
) { // assumed to be a calendar
7870 t
.isFetchNeeded
= isFetchNeeded
;
7871 t
.fetchEvents
= fetchEvents
;
7872 t
.addEventSource
= addEventSource
;
7873 t
.removeEventSource
= removeEventSource
;
7874 t
.updateEvent
= updateEvent
;
7875 t
.renderEvent
= renderEvent
;
7876 t
.removeEvents
= removeEvents
;
7877 t
.clientEvents
= clientEvents
;
7878 t
.mutateEvent
= mutateEvent
;
7879 t
.normalizeEventDateProps
= normalizeEventDateProps
;
7880 t
.ensureVisibleEventRange
= ensureVisibleEventRange
;
7884 var trigger
= t
.trigger
;
7885 var getView
= t
.getView
;
7886 var reportEvents
= t
.reportEvents
;
7890 var stickySource
= { events
: [] };
7891 var sources
= [ stickySource
];
7892 var rangeStart
, rangeEnd
;
7893 var currentFetchID
= 0;
7894 var pendingSourceCnt
= 0;
7895 var loadingLevel
= 0;
7896 var cache
= []; // holds events that have already been expanded
7900 (options
.events
? [ options
.events
] : []).concat(options
.eventSources
|| []),
7901 function(i
, sourceInput
) {
7902 var source
= buildEventSource(sourceInput
);
7904 sources
.push(source
);
7912 -----------------------------------------------------------------------------*/
7915 function isFetchNeeded(start
, end
) {
7916 return !rangeStart
|| // nothing has been fetched yet?
7917 // or, a part of the new range is outside of the old range? (after normalizing)
7918 start
.clone().stripZone() < rangeStart
.clone().stripZone() ||
7919 end
.clone().stripZone() > rangeEnd
.clone().stripZone();
7923 function fetchEvents(start
, end
) {
7927 var fetchID
= ++currentFetchID
;
7928 var len
= sources
.length
;
7929 pendingSourceCnt
= len
;
7930 for (var i
=0; i
<len
; i
++) {
7931 fetchEventSource(sources
[i
], fetchID
);
7936 function fetchEventSource(source
, fetchID
) {
7937 _fetchEventSource(source
, function(eventInputs
) {
7938 var isArraySource
= $.isArray(source
.events
);
7942 if (fetchID
== currentFetchID
) {
7945 for (i
= 0; i
< eventInputs
.length
; i
++) {
7946 eventInput
= eventInputs
[i
];
7948 if (isArraySource
) { // array sources have already been convert to Event Objects
7949 abstractEvent
= eventInput
;
7952 abstractEvent
= buildEventFromInput(eventInput
, source
);
7955 if (abstractEvent
) { // not false (an invalid event)
7958 expandEvent(abstractEvent
) // add individual expanded events to the cache
7965 if (!pendingSourceCnt
) {
7966 reportEvents(cache
);
7973 function _fetchEventSource(source
, callback
) {
7975 var fetchers
= fc
.sourceFetchers
;
7978 for (i
=0; i
<fetchers
.length
; i
++) {
7979 res
= fetchers
[i
].call(
7980 t
, // this, the Calendar object
7989 // the fetcher is in charge. made its own async request
7992 else if (typeof res
== 'object') {
7993 // the fetcher returned a new source. process it
7994 _fetchEventSource(res
, callback
);
7999 var events
= source
.events
;
8001 if ($.isFunction(events
)) {
8004 t
, // this, the Calendar object
8014 else if ($.isArray(events
)) {
8021 var url
= source
.url
;
8023 var success
= source
.success
;
8024 var error
= source
.error
;
8025 var complete
= source
.complete
;
8027 // retrieve any outbound GET/POST $.ajax data from the options
8029 if ($.isFunction(source
.data
)) {
8030 // supplied as a function that returns a key/value object
8031 customData
= source
.data();
8034 // supplied as a straight key/value object
8035 customData
= source
.data
;
8038 // use a copy of the custom data so we can modify the parameters
8039 // and not affect the passed-in object.
8040 var data
= $.extend({}, customData
|| {});
8042 var startParam
= firstDefined(source
.startParam
, options
.startParam
);
8043 var endParam
= firstDefined(source
.endParam
, options
.endParam
);
8044 var timezoneParam
= firstDefined(source
.timezoneParam
, options
.timezoneParam
);
8047 data
[startParam
] = rangeStart
.format();
8050 data
[endParam
] = rangeEnd
.format();
8052 if (options
.timezone
&& options
.timezone
!= 'local') {
8053 data
[timezoneParam
] = options
.timezone
;
8057 $.ajax($.extend({}, ajaxDefaults
, source
, {
8059 success: function(events
) {
8060 events
= events
|| [];
8061 var res
= applyAll(success
, this, arguments
);
8062 if ($.isArray(res
)) {
8068 applyAll(error
, this, arguments
);
8071 complete: function() {
8072 applyAll(complete
, this, arguments
);
8085 -----------------------------------------------------------------------------*/
8088 function addEventSource(sourceInput
) {
8089 var source
= buildEventSource(sourceInput
);
8091 sources
.push(source
);
8093 fetchEventSource(source
, currentFetchID
); // will eventually call reportEvents
8098 function buildEventSource(sourceInput
) { // will return undefined if invalid source
8099 var normalizers
= fc
.sourceNormalizers
;
8103 if ($.isFunction(sourceInput
) || $.isArray(sourceInput
)) {
8104 source
= { events
: sourceInput
};
8106 else if (typeof sourceInput
=== 'string') {
8107 source
= { url
: sourceInput
};
8109 else if (typeof sourceInput
=== 'object') {
8110 source
= $.extend({}, sourceInput
); // shallow copy
8115 // TODO: repeat code, same code for event classNames
8116 if (source
.className
) {
8117 if (typeof source
.className
=== 'string') {
8118 source
.className
= source
.className
.split(/\s+/);
8120 // otherwise, assumed to be an array
8123 source
.className
= [];
8126 // for array sources, we convert to standard Event Objects up front
8127 if ($.isArray(source
.events
)) {
8128 source
.origArray
= source
.events
; // for removeEventSource
8129 source
.events
= $.map(source
.events
, function(eventInput
) {
8130 return buildEventFromInput(eventInput
, source
);
8134 for (i
=0; i
<normalizers
.length
; i
++) {
8135 normalizers
[i
].call(t
, source
);
8143 function removeEventSource(source
) {
8144 sources
= $.grep(sources
, function(src
) {
8145 return !isSourcesEqual(src
, source
);
8147 // remove all client events from that source
8148 cache
= $.grep(cache
, function(e
) {
8149 return !isSourcesEqual(e
.source
, source
);
8151 reportEvents(cache
);
8155 function isSourcesEqual(source1
, source2
) {
8156 return source1
&& source2
&& getSourcePrimitive(source1
) == getSourcePrimitive(source2
);
8160 function getSourcePrimitive(source
) {
8162 (typeof source
=== 'object') ? // a normalized event source?
8163 (source
.origArray
|| source
.googleCalendarId
|| source
.url
|| source
.events
) : // get the primitive
8166 source
; // the given argument *is* the primitive
8172 -----------------------------------------------------------------------------*/
8175 // Only ever called from the externally-facing API
8176 function updateEvent(event
) {
8178 // massage start/end values, even if date string values
8179 event
.start
= t
.moment(event
.start
);
8181 event
.end
= t
.moment(event
.end
);
8187 mutateEvent(event
, getMiscEventProps(event
)); // will handle start/end/allDay normalization
8188 reportEvents(cache
); // reports event modifications (so we can redraw)
8192 // Returns a hash of misc event properties that should be copied over to related events.
8193 function getMiscEventProps(event
) {
8196 $.each(event
, function(name
, val
) {
8197 if (isMiscEventPropName(name
)) {
8198 if (val
!== undefined && isAtomic(val
)) { // a defined non-object
8207 // non-date-related, non-id-related, non-secret
8208 function isMiscEventPropName(name
) {
8209 return !/^_|^(id|allDay|start|end)$/.test(name
);
8213 // returns the expanded events that were created
8214 function renderEvent(eventInput
, stick
) {
8215 var abstractEvent
= buildEventFromInput(eventInput
);
8219 if (abstractEvent
) { // not false (a valid input)
8220 events
= expandEvent(abstractEvent
);
8222 for (i
= 0; i
< events
.length
; i
++) {
8225 if (!event
.source
) {
8227 stickySource
.events
.push(event
);
8228 event
.source
= stickySource
;
8234 reportEvents(cache
);
8243 function removeEvents(filter
) {
8247 if (filter
== null) { // null or undefined. remove all events
8248 filter = function() { return true; }; // will always match
8250 else if (!$.isFunction(filter
)) { // an event ID
8251 eventID
= filter
+ '';
8252 filter = function(event
) {
8253 return event
._id
== eventID
;
8257 // Purge event(s) from our local cache
8258 cache
= $.grep(cache
, filter
, true); // inverse=true
8260 // Remove events from array sources.
8261 // This works because they have been converted to official Event Objects up front.
8262 // (and as a result, event._id has been calculated).
8263 for (i
=0; i
<sources
.length
; i
++) {
8264 if ($.isArray(sources
[i
].events
)) {
8265 sources
[i
].events
= $.grep(sources
[i
].events
, filter
, true);
8269 reportEvents(cache
);
8273 function clientEvents(filter
) {
8274 if ($.isFunction(filter
)) {
8275 return $.grep(cache
, filter
);
8277 else if (filter
!= null) { // not null, not undefined. an event ID
8279 return $.grep(cache
, function(e
) {
8280 return e
._id
== filter
;
8283 return cache
; // else, return all
8289 -----------------------------------------------------------------------------*/
8292 function pushLoading() {
8293 if (!(loadingLevel
++)) {
8294 trigger('loading', null, true, getView());
8299 function popLoading() {
8300 if (!(--loadingLevel
)) {
8301 trigger('loading', null, false, getView());
8307 /* Event Normalization
8308 -----------------------------------------------------------------------------*/
8311 // Given a raw object with key/value properties, returns an "abstract" Event object.
8312 // An "abstract" event is an event that, if recurring, will not have been expanded yet.
8313 // Will return `false` when input is invalid.
8314 // `source` is optional
8315 function buildEventFromInput(input
, source
) {
8320 if (options
.eventDataTransform
) {
8321 input
= options
.eventDataTransform(input
);
8323 if (source
&& source
.eventDataTransform
) {
8324 input
= source
.eventDataTransform(input
);
8327 // Copy all properties over to the resulting object.
8328 // The special-case properties will be copied over afterwards.
8329 $.extend(out
, input
);
8332 out
.source
= source
;
8335 out
._id
= input
._id
|| (input
.id
=== undefined ? '_fc' + eventGUID
++ : input
.id
+ '');
8337 if (input
.className
) {
8338 if (typeof input
.className
== 'string') {
8339 out
.className
= input
.className
.split(/\s+/);
8341 else { // assumed to be an array
8342 out
.className
= input
.className
;
8349 start
= input
.start
|| input
.date
; // "date" is an alias for "start"
8352 // parse as a time (Duration) if applicable
8353 if (isTimeString(start
)) {
8354 start
= moment
.duration(start
);
8356 if (isTimeString(end
)) {
8357 end
= moment
.duration(end
);
8360 if (input
.dow
|| moment
.isDuration(start
) || moment
.isDuration(end
)) {
8362 // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
8363 out
.start
= start
? moment
.duration(start
) : null; // will be a Duration or null
8364 out
.end
= end
? moment
.duration(end
) : null; // will be a Duration or null
8365 out
._recurring
= true; // our internal marker
8370 start
= t
.moment(start
);
8371 if (!start
.isValid()) {
8377 end
= t
.moment(end
);
8378 if (!end
.isValid()) {
8379 end
= null; // let defaults take over
8383 allDay
= input
.allDay
;
8384 if (allDay
=== undefined) { // still undefined? fallback to default
8385 allDay
= firstDefined(
8386 source
? source
.allDayDefault
: undefined,
8387 options
.allDayDefault
8389 // still undefined? normalizeEventDateProps will calculate it
8392 assignDatesToEvent(start
, end
, allDay
, out
);
8399 // Normalizes and assigns the given dates to the given partially-formed event object.
8400 // NOTE: mutates the given start/end moments. does not make a copy.
8401 function assignDatesToEvent(start
, end
, allDay
, event
) {
8402 event
.start
= start
;
8404 event
.allDay
= allDay
;
8405 normalizeEventDateProps(event
);
8406 backupEventDates(event
);
8410 // Ensures the allDay property exists.
8411 // Ensures the start/end dates are consistent with allDay and forceEventDuration.
8412 // Accepts an Event object, or a plain object with event-ish properties.
8413 // NOTE: Will modify the given object.
8414 function normalizeEventDateProps(props
) {
8416 if (props
.allDay
== null) {
8417 props
.allDay
= !(props
.start
.hasTime() || (props
.end
&& props
.end
.hasTime()));
8421 props
.start
.stripTime();
8423 props
.end
.stripTime();
8427 if (!props
.start
.hasTime()) {
8428 props
.start
= t
.rezoneDate(props
.start
); // will also give it a 00:00 time
8430 if (props
.end
&& !props
.end
.hasTime()) {
8431 props
.end
= t
.rezoneDate(props
.end
); // will also give it a 00:00 time
8435 if (props
.end
&& !props
.end
.isAfter(props
.start
)) {
8440 if (options
.forceEventDuration
) {
8441 props
.end
= t
.getDefaultEventEnd(props
.allDay
, props
.start
);
8450 // If `range` is a proper range with a start and end, returns the original object.
8451 // If missing an end, computes a new range with an end, computing it as if it were an event.
8452 // TODO: make this a part of the event -> eventRange system
8453 function ensureVisibleEventRange(range
) {
8458 allDay
= range
.allDay
; // range might be more event-ish than we think
8459 if (allDay
== null) {
8460 allDay
= !range
.start
.hasTime();
8465 end
: t
.getDefaultEventEnd(allDay
, range
.start
)
8472 // If the given event is a recurring event, break it down into an array of individual instances.
8473 // If not a recurring event, return an array with the single original event.
8474 // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
8475 // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
8476 function expandEvent(abstractEvent
, _rangeStart
, _rangeEnd
) {
8482 var startTime
, endTime
;
8486 _rangeStart
= _rangeStart
|| rangeStart
;
8487 _rangeEnd
= _rangeEnd
|| rangeEnd
;
8489 if (abstractEvent
) {
8490 if (abstractEvent
._recurring
) {
8492 // make a boolean hash as to whether the event occurs on each day-of-week
8493 if ((dow
= abstractEvent
.dow
)) {
8495 for (i
= 0; i
< dow
.length
; i
++) {
8496 dowHash
[dow
[i
]] = true;
8500 // iterate through every day in the current range
8501 date
= _rangeStart
.clone().stripTime(); // holds the date of the current day
8502 while (date
.isBefore(_rangeEnd
)) {
8504 if (!dowHash
|| dowHash
[date
.day()]) { // if everyday, or this particular day-of-week
8506 startTime
= abstractEvent
.start
; // the stored start and end properties are times (Durations)
8507 endTime
= abstractEvent
.end
; // "
8508 start
= date
.clone();
8512 start
= start
.time(startTime
);
8515 end
= date
.clone().time(endTime
);
8518 event
= $.extend({}, abstractEvent
); // make a copy of the original
8521 !startTime
&& !endTime
, // allDay?
8527 date
.add(1, 'days');
8531 events
.push(abstractEvent
); // return the original event. will be a one-item array
8540 /* Event Modification Math
8541 -----------------------------------------------------------------------------------------*/
8544 // Modifies an event and all related events by applying the given properties.
8545 // Special date-diffing logic is used for manipulation of dates.
8546 // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
8547 // All date comparisons are done against the event's pristine _start and _end dates.
8548 // Returns an object with delta information and a function to undo all operations.
8550 function mutateEvent(event
, props
) {
8557 props
= props
|| {};
8559 // ensure new date-related values to compare against
8561 props
.start
= event
.start
.clone();
8563 if (props
.end
=== undefined) {
8564 props
.end
= event
.end
? event
.end
.clone() : null;
8566 if (props
.allDay
== null) { // is null or undefined?
8567 props
.allDay
= event
.allDay
;
8570 normalizeEventDateProps(props
); // massages start/end/allDay
8572 // clear the end date if explicitly changed to null
8573 clearEnd
= event
._end
!== null && props
.end
=== null;
8575 // compute the delta for moving the start and end dates together
8577 dateDelta
= diffDay(props
.start
, event
._start
); // whole-day diff from start-of-day
8580 dateDelta
= diffDayTime(props
.start
, event
._start
);
8583 // compute the delta for moving the end date (after applying dateDelta)
8584 if (!clearEnd
&& props
.end
) {
8585 durationDelta
= diffDayTime(
8589 ).subtract(diffDayTime(
8590 // subtract old duration
8591 event
._end
|| t
.getDefaultEventEnd(event
._allDay
, event
._start
),
8596 // gather all non-date-related properties
8597 $.each(props
, function(name
, val
) {
8598 if (isMiscEventPropName(name
)) {
8599 if (val
!== undefined) {
8600 miscProps
[name
] = val
;
8605 // apply the operations to the event and all related events
8606 undoFunc
= mutateEvents(
8607 clientEvents(event
._id
), // get events with this ID
8616 dateDelta
: dateDelta
,
8617 durationDelta
: durationDelta
,
8623 // Modifies an array of events in the following ways (operations are in order):
8624 // - clear the event's `end`
8625 // - convert the event to allDay
8626 // - add `dateDelta` to the start and end
8627 // - add `durationDelta` to the event's duration
8628 // - assign `miscProps` to the event
8630 // Returns a function that can be called to undo all the operations.
8632 // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
8634 function mutateEvents(events
, clearEnd
, allDay
, dateDelta
, durationDelta
, miscProps
) {
8635 var isAmbigTimezone
= t
.getIsAmbigTimezone();
8636 var undoFunctions
= [];
8638 // normalize zero-length deltas to be null
8639 if (dateDelta
&& !dateDelta
.valueOf()) { dateDelta
= null; }
8640 if (durationDelta
&& !durationDelta
.valueOf()) { durationDelta
= null; }
8642 $.each(events
, function(i
, event
) {
8646 // build an object holding all the old values, both date-related and misc.
8647 // for the undo function.
8649 start
: event
.start
.clone(),
8650 end
: event
.end
? event
.end
.clone() : null,
8651 allDay
: event
.allDay
8653 $.each(miscProps
, function(name
) {
8654 oldProps
[name
] = event
[name
];
8657 // new date-related properties. work off the original date snapshot.
8658 // ok to use references because they will be thrown away when backupEventDates is called.
8660 start
: event
._start
,
8662 allDay
: event
._allDay
8666 newProps
.end
= null;
8669 newProps
.allDay
= allDay
;
8671 normalizeEventDateProps(newProps
); // massages start/end/allDay
8674 newProps
.start
.add(dateDelta
);
8676 newProps
.end
.add(dateDelta
);
8680 if (durationDelta
) {
8681 if (!newProps
.end
) {
8682 newProps
.end
= t
.getDefaultEventEnd(newProps
.allDay
, newProps
.start
);
8684 newProps
.end
.add(durationDelta
);
8687 // if the dates have changed, and we know it is impossible to recompute the
8688 // timezone offsets, strip the zone.
8692 (dateDelta
|| durationDelta
)
8694 newProps
.start
.stripZone();
8696 newProps
.end
.stripZone();
8700 $.extend(event
, miscProps
, newProps
); // copy over misc props, then date-related props
8701 backupEventDates(event
); // regenerate internal _start/_end/_allDay
8703 undoFunctions
.push(function() {
8704 $.extend(event
, oldProps
);
8705 backupEventDates(event
); // regenerate internal _start/_end/_allDay
8710 for (var i
= 0; i
< undoFunctions
.length
; i
++) {
8718 -----------------------------------------------------------------------------------------*/
8720 t
.getBusinessHoursEvents
= getBusinessHoursEvents
;
8723 // Returns an array of events as to when the business hours occur in the given view.
8724 // Abuse of our event system :(
8725 function getBusinessHoursEvents() {
8726 var optionVal
= options
.businessHours
;
8728 className
: 'fc-nonbusiness',
8731 dow
: [ 1, 2, 3, 4, 5 ], // monday - friday
8732 rendering
: 'inverse-background'
8734 var view
= t
.getView();
8738 if (typeof optionVal
=== 'object') {
8739 // option value is an object that can override the default business hours
8740 eventInput
= $.extend({}, defaultVal
, optionVal
);
8743 // option value is `true`. use default business hours
8744 eventInput
= defaultVal
;
8750 buildEventFromInput(eventInput
),
8760 /* Overlapping / Constraining
8761 -----------------------------------------------------------------------------------------*/
8763 t
.isEventRangeAllowed
= isEventRangeAllowed
;
8764 t
.isSelectionRangeAllowed
= isSelectionRangeAllowed
;
8765 t
.isExternalDropRangeAllowed
= isExternalDropRangeAllowed
;
8768 function isEventRangeAllowed(range
, event
) {
8769 var source
= event
.source
|| {};
8770 var constraint
= firstDefined(
8773 options
.eventConstraint
8775 var overlap
= firstDefined(
8778 options
.eventOverlap
8781 range
= ensureVisibleEventRange(range
); // ensure a proper range with an end for isRangeAllowed
8783 return isRangeAllowed(range
, constraint
, overlap
, event
);
8787 function isSelectionRangeAllowed(range
) {
8788 return isRangeAllowed(range
, options
.selectConstraint
, options
.selectOverlap
);
8792 // when `eventProps` is defined, consider this an event.
8793 // `eventProps` can contain misc non-date-related info about the event.
8794 function isExternalDropRangeAllowed(range
, eventProps
) {
8798 // note: very similar logic is in View's reportExternalDrop
8800 eventInput
= $.extend({}, eventProps
, range
);
8801 event
= expandEvent(buildEventFromInput(eventInput
))[0];
8805 return isEventRangeAllowed(range
, event
);
8807 else { // treat it as a selection
8809 range
= ensureVisibleEventRange(range
); // ensure a proper range with an end for isSelectionRangeAllowed
8811 return isSelectionRangeAllowed(range
);
8816 // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
8817 // according to the constraint/overlap settings.
8818 // `event` is not required if checking a selection.
8819 function isRangeAllowed(range
, constraint
, overlap
, event
) {
8820 var constraintEvents
;
8825 // normalize. fyi, we're normalizing in too many places :(
8827 start
: range
.start
.clone().stripZone(),
8828 end
: range
.end
.clone().stripZone()
8831 // the range must be fully contained by at least one of produced constraint events
8832 if (constraint
!= null) {
8834 // not treated as an event! intermediate data structure
8835 // TODO: use ranges in the future
8836 constraintEvents
= constraintToEvents(constraint
);
8838 anyContainment
= false;
8839 for (i
= 0; i
< constraintEvents
.length
; i
++) {
8840 if (eventContainsRange(constraintEvents
[i
], range
)) {
8841 anyContainment
= true;
8846 if (!anyContainment
) {
8851 for (i
= 0; i
< cache
.length
; i
++) { // loop all events and detect overlap
8852 otherEvent
= cache
[i
];
8854 // don't compare the event to itself or other related [repeating] events
8855 if (event
&& event
._id
=== otherEvent
._id
) {
8859 // there needs to be an actual intersection before disallowing anything
8860 if (eventIntersectsRange(otherEvent
, range
)) {
8862 // evaluate overlap for the given range and short-circuit if necessary
8863 if (overlap
=== false) {
8866 else if (typeof overlap
=== 'function' && !overlap(otherEvent
, event
)) {
8870 // if we are computing if the given range is allowable for an event, consider the other event's
8871 // EventObject-specific or Source-specific `overlap` property
8873 otherOverlap
= firstDefined(
8875 (otherEvent
.source
|| {}).overlap
8876 // we already considered the global `eventOverlap`
8878 if (otherOverlap
=== false) {
8881 if (typeof otherOverlap
=== 'function' && !otherOverlap(event
, otherEvent
)) {
8892 // Given an event input from the API, produces an array of event objects. Possible event inputs:
8894 // An event ID (number or string)
8895 // An object with specific start/end dates or a recurring event (like what businessHours accepts)
8896 function constraintToEvents(constraintInput
) {
8898 if (constraintInput
=== 'businessHours') {
8899 return getBusinessHoursEvents();
8902 if (typeof constraintInput
=== 'object') {
8903 return expandEvent(buildEventFromInput(constraintInput
));
8906 return clientEvents(constraintInput
); // probably an ID
8910 // Does the event's date range fully contain the given range?
8911 // start/end already assumed to have stripped zones :(
8912 function eventContainsRange(event
, range
) {
8913 var eventStart
= event
.start
.clone().stripZone();
8914 var eventEnd
= t
.getEventEnd(event
).stripZone();
8916 return range
.start
>= eventStart
&& range
.end
<= eventEnd
;
8920 // Does the event's date range intersect with the given range?
8921 // start/end already assumed to have stripped zones :(
8922 function eventIntersectsRange(event
, range
) {
8923 var eventStart
= event
.start
.clone().stripZone();
8924 var eventEnd
= t
.getEventEnd(event
).stripZone();
8926 return range
.start
< eventEnd
&& range
.end
> eventStart
;
8932 // updates the "backup" properties, which are preserved in order to compute diffs later on.
8933 function backupEventDates(event
) {
8934 event
._allDay
= event
.allDay
;
8935 event
._start
= event
.start
.clone();
8936 event
._end
= event
.end
? event
.end
.clone() : null;
8939 /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
8940 ----------------------------------------------------------------------------------------------------------------------*/
8941 // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
8942 // It is responsible for managing width/height.
8944 var BasicView
= fcViews
.basic
= View
.extend({
8946 dayGrid
: null, // the main subcomponent that does most of the heavy lifting
8948 dayNumbersVisible
: false, // display day numbers on each day cell?
8949 weekNumbersVisible
: false, // display week numbers along the side?
8951 weekNumberWidth
: null, // width of all the week-number cells running down the side
8953 headRowEl
: null, // the fake row element of the day-of-week header
8956 initialize: function() {
8957 this.dayGrid
= new DayGrid(this);
8958 this.coordMap
= this.dayGrid
.coordMap
; // the view's date-to-cell mapping is identical to the subcomponent's
8962 // Sets the display range and computes all necessary dates
8963 setRange: function(range
) {
8964 View
.prototype.setRange
.call(this, range
); // call the super-method
8966 this.dayGrid
.breakOnWeeks
= /year|month|week/.test(this.intervalUnit
); // do before setRange
8967 this.dayGrid
.setRange(range
);
8971 // Compute the value to feed into setRange. Overrides superclass.
8972 computeRange: function(date
) {
8973 var range
= View
.prototype.computeRange
.call(this, date
); // get value from the super-method
8975 // year and month views should be aligned with weeks. this is already done for week
8976 if (/year|month/.test(range
.intervalUnit
)) {
8977 range
.start
.startOf('week');
8978 range
.start
= this.skipHiddenDays(range
.start
);
8980 // make end-of-week if not already
8981 if (range
.end
.weekday()) {
8982 range
.end
.add(1, 'week').startOf('week');
8983 range
.end
= this.skipHiddenDays(range
.end
, -1, true); // exclusively move backwards
8991 // Renders the view into `this.el`, which should already be assigned
8992 render: function() {
8994 this.dayNumbersVisible
= this.dayGrid
.rowCnt
> 1; // TODO: make grid responsible
8995 this.weekNumbersVisible
= this.opt('weekNumbers');
8996 this.dayGrid
.numbersVisible
= this.dayNumbersVisible
|| this.weekNumbersVisible
;
8998 this.el
.addClass('fc-basic-view').html(this.renderHtml());
9000 this.headRowEl
= this.el
.find('thead .fc-row');
9002 this.scrollerEl
= this.el
.find('.fc-day-grid-container');
9003 this.dayGrid
.coordMap
.containerEl
= this.scrollerEl
; // constrain clicks/etc to the dimensions of the scroller
9005 this.dayGrid
.el
= this.el
.find('.fc-day-grid');
9006 this.dayGrid
.render(this.hasRigidRows());
9010 // Make subcomponents ready for cleanup
9011 destroy: function() {
9012 this.dayGrid
.destroy();
9013 View
.prototype.destroy
.call(this); // call the super-method
9017 // Builds the HTML skeleton for the view.
9018 // The day-grid component will render inside of a container defined by this HTML.
9019 renderHtml: function() {
9024 '<td class="' + this.widgetHeaderClass
+ '">' +
9025 this.dayGrid
.headHtml() + // render the day-of-week headers
9031 '<td class="' + this.widgetContentClass
+ '">' +
9032 '<div class="fc-day-grid-container">' +
9033 '<div class="fc-day-grid"/>' +
9042 // Generates the HTML that will go before the day-of week header cells.
9043 // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
9044 headIntroHtml: function() {
9045 if (this.weekNumbersVisible
) {
9047 '<th class="fc-week-number ' + this.widgetHeaderClass
+ '" ' + this.weekNumberStyleAttr() + '>' +
9048 '<span>' + // needed for matchCellWidths
9049 htmlEscape(this.opt('weekNumberTitle')) +
9056 // Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
9057 // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
9058 numberIntroHtml: function(row
) {
9059 if (this.weekNumbersVisible
) {
9061 '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
9062 '<span>' + // needed for matchCellWidths
9063 this.calendar
.calculateWeekNumber(this.dayGrid
.getCell(row
, 0).start
) +
9070 // Generates the HTML that goes before the day bg cells for each day-row.
9071 // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
9072 dayIntroHtml: function() {
9073 if (this.weekNumbersVisible
) {
9074 return '<td class="fc-week-number ' + this.widgetContentClass
+ '" ' +
9075 this.weekNumberStyleAttr() + '></td>';
9080 // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
9081 // Affects helper-skeleton and highlight-skeleton rows.
9082 introHtml: function() {
9083 if (this.weekNumbersVisible
) {
9084 return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
9089 // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
9090 // The number row will only exist if either day numbers or week numbers are turned on.
9091 numberCellHtml: function(cell
) {
9092 var date
= cell
.start
;
9095 if (!this.dayNumbersVisible
) { // if there are week numbers but not day numbers
9096 return '<td/>'; // will create an empty space above events :(
9099 classes
= this.dayGrid
.getDayClasses(date
);
9100 classes
.unshift('fc-day-number');
9103 '<td class="' + classes
.join(' ') + '" data-date="' + date
.format() + '">' +
9109 // Generates an HTML attribute string for setting the width of the week number column, if it is known
9110 weekNumberStyleAttr: function() {
9111 if (this.weekNumberWidth
!== null) {
9112 return 'style="width:' + this.weekNumberWidth
+ 'px"';
9118 // Determines whether each row should have a constant height
9119 hasRigidRows: function() {
9120 var eventLimit
= this.opt('eventLimit');
9121 return eventLimit
&& typeof eventLimit
!== 'number';
9126 ------------------------------------------------------------------------------------------------------------------*/
9129 // Refreshes the horizontal dimensions of the view
9130 updateWidth: function() {
9131 if (this.weekNumbersVisible
) {
9132 // Make sure all week number cells running down the side have the same width.
9133 // Record the width for cells created later.
9134 this.weekNumberWidth
= matchCellWidths(
9135 this.el
.find('.fc-week-number')
9141 // Adjusts the vertical dimensions of the view to the specified values
9142 setHeight: function(totalHeight
, isAuto
) {
9143 var eventLimit
= this.opt('eventLimit');
9146 // reset all heights to be natural
9147 unsetScroller(this.scrollerEl
);
9148 uncompensateScroll(this.headRowEl
);
9150 this.dayGrid
.destroySegPopover(); // kill the "more" popover if displayed
9152 // is the event limit a constant level number?
9153 if (eventLimit
&& typeof eventLimit
=== 'number') {
9154 this.dayGrid
.limitRows(eventLimit
); // limit the levels first so the height can redistribute after
9157 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
9158 this.setGridHeight(scrollerHeight
, isAuto
);
9160 // is the event limit dynamically calculated?
9161 if (eventLimit
&& typeof eventLimit
!== 'number') {
9162 this.dayGrid
.limitRows(eventLimit
); // limit the levels after the grid's row heights have been set
9165 if (!isAuto
&& setPotentialScroller(this.scrollerEl
, scrollerHeight
)) { // using scrollbars?
9167 compensateScroll(this.headRowEl
, getScrollbarWidths(this.scrollerEl
));
9169 // doing the scrollbar compensation might have created text overflow which created more height. redo
9170 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
9171 this.scrollerEl
.height(scrollerHeight
);
9173 this.restoreScroll();
9178 // Sets the height of just the DayGrid component in this view
9179 setGridHeight: function(height
, isAuto
) {
9181 undistributeHeight(this.dayGrid
.rowEls
); // let the rows be their natural height with no expanding
9184 distributeHeight(this.dayGrid
.rowEls
, height
, true); // true = compensate for height-hogging rows
9190 ------------------------------------------------------------------------------------------------------------------*/
9193 // Renders the given events onto the view and populates the segments array
9194 renderEvents: function(events
) {
9195 this.dayGrid
.renderEvents(events
);
9197 this.updateHeight(); // must compensate for events that overflow the row
9201 // Retrieves all segment objects that are rendered in the view
9202 getEventSegs: function() {
9203 return this.dayGrid
.getEventSegs();
9207 // Unrenders all event elements and clears internal segment data
9208 destroyEvents: function() {
9209 this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
9210 this.dayGrid
.destroyEvents();
9212 // we DON'T need to call updateHeight() because:
9213 // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
9214 // B) in IE8, this causes a flash whenever events are rerendered
9218 /* Dragging (for both events and external elements)
9219 ------------------------------------------------------------------------------------------------------------------*/
9222 // A returned value of `true` signals that a mock "helper" event has been rendered.
9223 renderDrag: function(dropLocation
, seg
) {
9224 return this.dayGrid
.renderDrag(dropLocation
, seg
);
9228 destroyDrag: function() {
9229 this.dayGrid
.destroyDrag();
9234 ------------------------------------------------------------------------------------------------------------------*/
9237 // Renders a visual indication of a selection
9238 renderSelection: function(range
) {
9239 this.dayGrid
.renderSelection(range
);
9243 // Unrenders a visual indications of a selection
9244 destroySelection: function() {
9245 this.dayGrid
.destroySelection();
9250 /* A month view with day cells running in rows (one-per-week) and columns
9251 ----------------------------------------------------------------------------------------------------------------------*/
9254 fixedWeekCount
: true
9257 var MonthView
= fcViews
.month
= BasicView
.extend({
9259 // Produces information about what range to display
9260 computeRange: function(date
) {
9261 var range
= BasicView
.prototype.computeRange
.call(this, date
); // get value from super-method
9263 if (this.isFixedWeeks()) {
9266 6 - range
.end
.diff(range
.start
, 'weeks'),
9275 // Overrides the default BasicView behavior to have special multi-week auto-height logic
9276 setGridHeight: function(height
, isAuto
) {
9278 isAuto
= isAuto
|| this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
9280 // if auto, make the height of each row the height that it would be if there were 6 weeks
9282 height
*= this.rowCnt
/ 6;
9285 distributeHeight(this.dayGrid
.rowEls
, height
, !isAuto
); // if auto, don't compensate for height-hogging rows
9289 isFixedWeeks: function() {
9290 var weekMode
= this.opt('weekMode'); // LEGACY: weekMode is deprecated
9292 return weekMode
=== 'fixed'; // if any other type of weekMode, assume NOT fixed
9295 return this.opt('fixedWeekCount');
9300 MonthView
.duration
= { months
: 1 };
9302 /* A week view with simple day cells running horizontally
9303 ----------------------------------------------------------------------------------------------------------------------*/
9305 fcViews
.basicWeek
= {
9307 duration
: { weeks
: 1 }
9309 /* A view with a single simple day cell
9310 ----------------------------------------------------------------------------------------------------------------------*/
9312 fcViews
.basicDay
= {
9314 duration
: { days
: 1 }
9316 /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
9317 ----------------------------------------------------------------------------------------------------------------------*/
9318 // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
9319 // Responsible for managing width/height.
9323 allDayText
: 'all-day',
9324 scrollTime
: '06:00:00',
9325 slotDuration
: '00:30:00',
9326 minTime
: '00:00:00',
9327 maxTime
: '24:00:00',
9328 slotEventOverlap
: true
9331 var AGENDA_ALL_DAY_EVENT_LIMIT
= 5;
9333 fcViews
.agenda
= View
.extend({ // AgendaView
9335 timeGrid
: null, // the main time-grid subcomponent of this view
9336 dayGrid
: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
9338 axisWidth
: null, // the width of the time axis running down the side
9340 noScrollRowEls
: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
9342 // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
9344 bottomRuleHeight
: null,
9347 initialize: function() {
9348 this.timeGrid
= new TimeGrid(this);
9350 if (this.opt('allDaySlot')) { // should we display the "all-day" area?
9351 this.dayGrid
= new DayGrid(this); // the all-day subcomponent of this view
9353 // the coordinate grid will be a combination of both subcomponents' grids
9354 this.coordMap
= new ComboCoordMap([
9355 this.dayGrid
.coordMap
,
9356 this.timeGrid
.coordMap
9360 this.coordMap
= this.timeGrid
.coordMap
;
9366 ------------------------------------------------------------------------------------------------------------------*/
9369 // Sets the display range and computes all necessary dates
9370 setRange: function(range
) {
9371 View
.prototype.setRange
.call(this, range
); // call the super-method
9373 this.timeGrid
.setRange(range
);
9375 this.dayGrid
.setRange(range
);
9380 // Renders the view into `this.el`, which has already been assigned
9381 render: function() {
9383 this.el
.addClass('fc-agenda-view').html(this.renderHtml());
9385 // the element that wraps the time-grid that will probably scroll
9386 this.scrollerEl
= this.el
.find('.fc-time-grid-container');
9387 this.timeGrid
.coordMap
.containerEl
= this.scrollerEl
; // don't accept clicks/etc outside of this
9389 this.timeGrid
.el
= this.el
.find('.fc-time-grid');
9390 this.timeGrid
.render();
9392 // the <hr> that sometimes displays under the time-grid
9393 this.bottomRuleEl
= $('<hr class="' + this.widgetHeaderClass
+ '"/>')
9394 .appendTo(this.timeGrid
.el
); // inject it into the time-grid
9397 this.dayGrid
.el
= this.el
.find('.fc-day-grid');
9398 this.dayGrid
.render();
9400 // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
9401 this.dayGrid
.bottomCoordPadding
= this.dayGrid
.el
.next('hr').outerHeight();
9404 this.noScrollRowEls
= this.el
.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
9408 // Make subcomponents ready for cleanup
9409 destroy: function() {
9410 this.timeGrid
.destroy();
9412 this.dayGrid
.destroy();
9414 View
.prototype.destroy
.call(this); // call the super-method
9418 // Builds the HTML skeleton for the view.
9419 // The day-grid and time-grid components will render inside containers defined by this HTML.
9420 renderHtml: function() {
9425 '<td class="' + this.widgetHeaderClass
+ '">' +
9426 this.timeGrid
.headHtml() + // render the day-of-week headers
9432 '<td class="' + this.widgetContentClass
+ '">' +
9434 '<div class="fc-day-grid"/>' +
9435 '<hr class="' + this.widgetHeaderClass
+ '"/>' :
9438 '<div class="fc-time-grid-container">' +
9439 '<div class="fc-time-grid"/>' +
9448 // Generates the HTML that will go before the day-of week header cells.
9449 // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
9450 headIntroHtml: function() {
9456 if (this.opt('weekNumbers')) {
9457 date
= this.timeGrid
.getCell(0).start
;
9458 weekNumber
= this.calendar
.calculateWeekNumber(date
);
9459 weekTitle
= this.opt('weekNumberTitle');
9461 if (this.opt('isRTL')) {
9462 weekText
= weekNumber
+ weekTitle
;
9465 weekText
= weekTitle
+ weekNumber
;
9469 '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass
+ '" ' + this.axisStyleAttr() + '>' +
9470 '<span>' + // needed for matchCellWidths
9471 htmlEscape(weekText
) +
9476 return '<th class="fc-axis ' + this.widgetHeaderClass
+ '" ' + this.axisStyleAttr() + '></th>';
9481 // Generates the HTML that goes before the all-day cells.
9482 // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
9483 dayIntroHtml: function() {
9485 '<td class="fc-axis ' + this.widgetContentClass
+ '" ' + this.axisStyleAttr() + '>' +
9486 '<span>' + // needed for matchCellWidths
9487 (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
9493 // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
9494 slotBgIntroHtml: function() {
9495 return '<td class="fc-axis ' + this.widgetContentClass
+ '" ' + this.axisStyleAttr() + '></td>';
9499 // Generates the HTML that goes before all other types of cells.
9500 // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
9501 // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
9502 introHtml: function() {
9503 return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
9507 // Generates an HTML attribute string for setting the width of the axis, if it is known
9508 axisStyleAttr: function() {
9509 if (this.axisWidth
!== null) {
9510 return 'style="width:' + this.axisWidth
+ 'px"';
9517 ------------------------------------------------------------------------------------------------------------------*/
9520 updateSize: function(isResize
) {
9522 this.timeGrid
.resize();
9524 View
.prototype.updateSize
.call(this, isResize
);
9528 // Refreshes the horizontal dimensions of the view
9529 updateWidth: function() {
9530 // make all axis cells line up, and record the width so newly created axis cells will have it
9531 this.axisWidth
= matchCellWidths(this.el
.find('.fc-axis'));
9535 // Adjusts the vertical dimensions of the view to the specified values
9536 setHeight: function(totalHeight
, isAuto
) {
9540 if (this.bottomRuleHeight
=== null) {
9541 // calculate the height of the rule the very first time
9542 this.bottomRuleHeight
= this.bottomRuleEl
.outerHeight();
9544 this.bottomRuleEl
.hide(); // .show() will be called later if this <hr> is necessary
9546 // reset all dimensions back to the original state
9547 this.scrollerEl
.css('overflow', '');
9548 unsetScroller(this.scrollerEl
);
9549 uncompensateScroll(this.noScrollRowEls
);
9551 // limit number of events in the all-day area
9553 this.dayGrid
.destroySegPopover(); // kill the "more" popover if displayed
9555 eventLimit
= this.opt('eventLimit');
9556 if (eventLimit
&& typeof eventLimit
!== 'number') {
9557 eventLimit
= AGENDA_ALL_DAY_EVENT_LIMIT
; // make sure "auto" goes to a real number
9560 this.dayGrid
.limitRows(eventLimit
);
9564 if (!isAuto
) { // should we force dimensions of the scroll container, or let the contents be natural height?
9566 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
9567 if (setPotentialScroller(this.scrollerEl
, scrollerHeight
)) { // using scrollbars?
9569 // make the all-day and header rows lines up
9570 compensateScroll(this.noScrollRowEls
, getScrollbarWidths(this.scrollerEl
));
9572 // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
9573 // and reapply the desired height to the scroller.
9574 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
9575 this.scrollerEl
.height(scrollerHeight
);
9577 this.restoreScroll();
9579 else { // no scrollbars
9580 // still, force a height and display the bottom rule (marks the end of day)
9581 this.scrollerEl
.height(scrollerHeight
).css('overflow', 'hidden'); // in case <hr> goes outside
9582 this.bottomRuleEl
.show();
9588 // Sets the scroll value of the scroller to the initial pre-configured state prior to allowing the user to change it
9589 initializeScroll: function() {
9591 var scrollTime
= moment
.duration(this.opt('scrollTime'));
9592 var top
= this.timeGrid
.computeTimeTop(scrollTime
);
9594 // zoom can give weird floating-point values. rather scroll a little bit further
9595 top
= Math
.ceil(top
);
9598 top
++; // to overcome top border that slots beyond the first have. looks better
9602 _this
.scrollerEl
.scrollTop(top
);
9606 setTimeout(scroll
, 0); // overrides any previous scroll state made by the browser
9611 ------------------------------------------------------------------------------------------------------------------*/
9614 // Renders events onto the view and populates the View's segment array
9615 renderEvents: function(events
) {
9617 var timedEvents
= [];
9622 // separate the events into all-day and timed
9623 for (i
= 0; i
< events
.length
; i
++) {
9624 if (events
[i
].allDay
) {
9625 dayEvents
.push(events
[i
]);
9628 timedEvents
.push(events
[i
]);
9632 // render the events in the subcomponents
9633 timedSegs
= this.timeGrid
.renderEvents(timedEvents
);
9635 daySegs
= this.dayGrid
.renderEvents(dayEvents
);
9638 // the all-day area is flexible and might have a lot of events, so shift the height
9639 this.updateHeight();
9643 // Retrieves all segment objects that are rendered in the view
9644 getEventSegs: function() {
9645 return this.timeGrid
.getEventSegs().concat(
9646 this.dayGrid
? this.dayGrid
.getEventSegs() : []
9651 // Unrenders all event elements and clears internal segment data
9652 destroyEvents: function() {
9654 // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly
9655 // after, so remember what the scroll value was so we can restore it.
9656 this.recordScroll();
9658 // destroy the events in the subcomponents
9659 this.timeGrid
.destroyEvents();
9661 this.dayGrid
.destroyEvents();
9664 // we DON'T need to call updateHeight() because:
9665 // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
9666 // B) in IE8, this causes a flash whenever events are rerendered
9670 /* Dragging (for events and external elements)
9671 ------------------------------------------------------------------------------------------------------------------*/
9674 // A returned value of `true` signals that a mock "helper" event has been rendered.
9675 renderDrag: function(dropLocation
, seg
) {
9676 if (dropLocation
.start
.hasTime()) {
9677 return this.timeGrid
.renderDrag(dropLocation
, seg
);
9679 else if (this.dayGrid
) {
9680 return this.dayGrid
.renderDrag(dropLocation
, seg
);
9685 destroyDrag: function() {
9686 this.timeGrid
.destroyDrag();
9688 this.dayGrid
.destroyDrag();
9694 ------------------------------------------------------------------------------------------------------------------*/
9697 // Renders a visual indication of a selection
9698 renderSelection: function(range
) {
9699 if (range
.start
.hasTime() || range
.end
.hasTime()) {
9700 this.timeGrid
.renderSelection(range
);
9702 else if (this.dayGrid
) {
9703 this.dayGrid
.renderSelection(range
);
9708 // Unrenders a visual indications of a selection
9709 destroySelection: function() {
9710 this.timeGrid
.destroySelection();
9712 this.dayGrid
.destroySelection();
9718 /* A week view with an all-day cell area at the top, and a time grid below
9719 ----------------------------------------------------------------------------------------------------------------------*/
9721 fcViews
.agendaWeek
= {
9723 duration
: { weeks
: 1 }
9725 /* A day view with an all-day cell area at the top, and a time grid below
9726 ----------------------------------------------------------------------------------------------------------------------*/
9728 fcViews
.agendaDay
= {
9730 duration
: { days
: 1 }